diff --git a/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx b/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx index 5f60e71a8..1f03dc003 100644 --- a/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx +++ b/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx @@ -17,8 +17,10 @@ import CompassIcon from '../../../../webapp/src/widgets/icons/compassIcon' import {Permission} from '../../../../webapp/src/constants' -import './rhsChannelBoardItem.scss' import BoardPermissionGate from '../../../../webapp/src/components/permissions/boardPermissionGate' +import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../../../webapp/src/telemetry/telemetryClient' + +import './rhsChannelBoardItem.scss' const windowAny = (window as SuiteWindow) @@ -36,6 +38,10 @@ const RHSChannelBoardItem = (props: Props) => { } const handleBoardClicked = (boardID: string) => { + // send the telemetry information for the clicked board + const extraData = {teamID: team.id, board: boardID} + TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelsRHSBoard, extraData) + window.open(`${windowAny.frontendBaseURL}/team/${team.id}/${boardID}`, '_blank', 'noopener') } diff --git a/server/api/api.go b/server/api/api.go index ec376a9c0..be0287257 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -227,7 +227,7 @@ func jsonStringResponse(w http.ResponseWriter, code int, message string) { //nol fmt.Fprint(w, message) } -func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { +func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { //nolint:unparam setResponseHeader(w, "Content-Type", "application/json") w.WriteHeader(code) _, _ = w.Write(json) diff --git a/server/api/files.go b/server/api/files.go index d9f6aa109..bb0ddcfc7 100644 --- a/server/api/files.go +++ b/server/api/files.go @@ -123,37 +123,12 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { auditRec.AddMeta("teamID", board.TeamID) auditRec.AddMeta("filename", filename) - fileInfo, err := a.app.GetFileInfo(filename) + fileInfo, fileReader, err := a.app.GetFile(board.TeamID, boardID, filename) if err != nil && !model.IsErrNotFound(err) { a.errorResponse(w, r, err) return } - if fileInfo != nil && fileInfo.Archived { - fileMetadata := map[string]interface{}{ - "archived": true, - "name": fileInfo.Name, - "size": fileInfo.Size, - "extension": fileInfo.Extension, - } - - data, jsonErr := json.Marshal(fileMetadata) - if jsonErr != nil { - a.logger.Error("failed to marshal archived file metadata", mlog.String("filename", filename), mlog.Err(jsonErr)) - a.errorResponse(w, r, jsonErr) - return - } - - jsonBytesResponse(w, http.StatusBadRequest, data) - return - } - - fileReader, err := a.app.GetFileReader(board.TeamID, boardID, filename) - if err != nil && !errors.Is(err, app.ErrFileNotFound) { - a.errorResponse(w, r, err) - return - } - if errors.Is(err, app.ErrFileNotFound) && board.ChannelID != "" { // prior to moving from workspaces to teams, the filepath was constructed from // workspaceID, which is the channel ID in plugin mode. diff --git a/server/app/files.go b/server/app/files.go index 081b78e14..1474a291a 100644 --- a/server/app/files.go +++ b/server/app/files.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + "github.com/mattermost/focalboard/server/model" mmModel "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/focalboard/server/utils" @@ -28,7 +29,7 @@ func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (strin createdFilename := utils.NewID(utils.IDTypeNone) fullFilename := fmt.Sprintf(`%s%s`, createdFilename, fileExtension) - filePath := filepath.Join(teamID, rootID, fullFilename) + filePath := filepath.Join(utils.GetBaseFilePath(), fullFilename) fileSize, appErr := a.filesBackend.WriteFile(reader, filePath) if appErr != nil { @@ -45,7 +46,7 @@ func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (strin CreateAt: now, UpdateAt: now, DeleteAt: 0, - Path: emptyString, + Path: filePath, ThumbnailPath: emptyString, PreviewPath: emptyString, Name: filename, @@ -59,6 +60,7 @@ func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (strin Content: "", RemoteId: nil, } + err := a.store.SaveFileInfo(fileInfo) if err != nil { return "", err @@ -77,6 +79,7 @@ func (a *App) GetFileInfo(filename string) (*mmModel.FileInfo, error) { // will be the fileinfo id. parts := strings.Split(filename, ".") fileInfoID := parts[0][1:] + fileInfo, err := a.store.GetFileInfo(fileInfoID) if err != nil { return nil, err @@ -85,6 +88,40 @@ func (a *App) GetFileInfo(filename string) (*mmModel.FileInfo, error) { return fileInfo, nil } +func (a *App) GetFile(teamID, rootID, fileName string) (*mmModel.FileInfo, filestore.ReadCloseSeeker, error) { + fileInfo, err := a.GetFileInfo(fileName) + if err != nil && !model.IsErrNotFound(err) { + a.logger.Error("111") + return nil, nil, err + } + + var filePath string + + if fileInfo != nil && fileInfo.Path != "" { + filePath = fileInfo.Path + } else { + filePath = filepath.Join(teamID, rootID, fileName) + } + + exists, err := a.filesBackend.FileExists(filePath) + if err != nil { + a.logger.Error(fmt.Sprintf("GetFile: Failed to check if file exists as path. Path: %s, error: %e", filePath, err)) + return nil, nil, err + } + + if !exists { + return nil, nil, ErrFileNotFound + } + + reader, err := a.filesBackend.Reader(filePath) + if err != nil { + a.logger.Error(fmt.Sprintf("GetFile: Failed to get file reader of existing file at path: %s, error: %e", filePath, err)) + return nil, nil, err + } + + return fileInfo, reader, nil +} + func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) { filePath := filepath.Join(teamID, rootID, filename) exists, err := a.filesBackend.FileExists(filePath) diff --git a/server/app/files_test.go b/server/app/files_test.go index 11e4991b8..b39327f7b 100644 --- a/server/app/files_test.go +++ b/server/app/files_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" @@ -195,8 +196,8 @@ func TestSaveFile(t *testing.T) { writeFileFunc := func(reader io.Reader, path string) int64 { paths := strings.Split(path, string(os.PathSeparator)) - assert.Equal(t, "1", paths[0]) - assert.Equal(t, testBoardID, paths[1]) + assert.Equal(t, "boards", paths[0]) + assert.Equal(t, time.Now().Format("20060102"), paths[1]) fileName = paths[2] return int64(10) } @@ -219,8 +220,8 @@ func TestSaveFile(t *testing.T) { writeFileFunc := func(reader io.Reader, path string) int64 { paths := strings.Split(path, string(os.PathSeparator)) - assert.Equal(t, "1", paths[0]) - assert.Equal(t, "test-board-id", paths[1]) + assert.Equal(t, "boards", paths[0]) + assert.Equal(t, time.Now().Format("20060102"), paths[1]) assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1]) return int64(10) } @@ -243,8 +244,8 @@ func TestSaveFile(t *testing.T) { writeFileFunc := func(reader io.Reader, path string) int64 { paths := strings.Split(path, string(os.PathSeparator)) - assert.Equal(t, "1", paths[0]) - assert.Equal(t, "test-board-id", paths[1]) + assert.Equal(t, "boards", paths[0]) + assert.Equal(t, time.Now().Format("20060102"), paths[1]) assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1]) return int64(10) } @@ -304,3 +305,80 @@ func TestGetFileInfo(t *testing.T) { assert.Nil(t, fetchedFileInfo) }) } + +func TestGetFile(t *testing.T) { + th, _ := SetupTestHelper(t) + + t.Run("when FileInfo exists", func(t *testing.T) { + th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mmModel.FileInfo{ + Id: "fileInfoID", + Path: "/path/to/file/fileName.txt", + }, nil) + + mockedFileBackend := &mocks.FileBackend{} + th.App.filesBackend = mockedFileBackend + mockedReadCloseSeek := &mocks.ReadCloseSeeker{} + readerFunc := func(path string) filestore.ReadCloseSeeker { + return mockedReadCloseSeek + } + + readerErrorFunc := func(path string) error { + return nil + } + mockedFileBackend.On("Reader", "/path/to/file/fileName.txt").Return(readerFunc, readerErrorFunc) + mockedFileBackend.On("FileExists", "/path/to/file/fileName.txt").Return(true, nil) + + fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt") + assert.NoError(t, err) + assert.NotNil(t, fileInfo) + assert.NotNil(t, seeker) + }) + + t.Run("when FileInfo doesn't exist", func(t *testing.T) { + th.Store.EXPECT().GetFileInfo("fileInfoID").Return(nil, nil) + + mockedFileBackend := &mocks.FileBackend{} + th.App.filesBackend = mockedFileBackend + mockedReadCloseSeek := &mocks.ReadCloseSeeker{} + readerFunc := func(path string) filestore.ReadCloseSeeker { + return mockedReadCloseSeek + } + + readerErrorFunc := func(path string) error { + return nil + } + + mockedFileBackend.On("Reader", "teamID/boardID/7fileInfoID.txt").Return(readerFunc, readerErrorFunc) + mockedFileBackend.On("FileExists", "teamID/boardID/7fileInfoID.txt").Return(true, nil) + + fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt") + assert.NoError(t, err) + assert.Nil(t, fileInfo) + assert.NotNil(t, seeker) + }) + + t.Run("when FileInfo exists but FileInfo.Path is not set", func(t *testing.T) { + th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mmModel.FileInfo{ + Id: "fileInfoID", + Path: "", + }, nil) + + mockedFileBackend := &mocks.FileBackend{} + th.App.filesBackend = mockedFileBackend + mockedReadCloseSeek := &mocks.ReadCloseSeeker{} + readerFunc := func(path string) filestore.ReadCloseSeeker { + return mockedReadCloseSeek + } + + readerErrorFunc := func(path string) error { + return nil + } + mockedFileBackend.On("Reader", "teamID/boardID/7fileInfoID.txt").Return(readerFunc, readerErrorFunc) + mockedFileBackend.On("FileExists", "teamID/boardID/7fileInfoID.txt").Return(true, nil) + + fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt") + assert.NoError(t, err) + assert.NotNil(t, fileInfo) + assert.NotNil(t, seeker) + }) +} diff --git a/server/client/client.go b/server/client/client.go index 6be594f40..8c794ccfe 100644 --- a/server/client/client.go +++ b/server/client/client.go @@ -380,6 +380,8 @@ func (c *Client) GetCards(boardID string, page int, perPage int) ([]*model.Card, return nil, BuildErrorResponse(r, err) } + defer closeBody(r) + var cards []*model.Card if err := json.NewDecoder(r.Body).Decode(&cards); err != nil { return nil, BuildErrorResponse(r, err) @@ -398,6 +400,8 @@ func (c *Client) PatchCard(cardID string, cardPatch *model.CardPatch, disableNot return nil, BuildErrorResponse(r, err) } + defer closeBody(r) + var cardNew *model.Card if err := json.NewDecoder(r.Body).Decode(&cardNew); err != nil { return nil, BuildErrorResponse(r, err) @@ -412,6 +416,8 @@ func (c *Client) GetCard(cardID string) (*model.Card, *Response) { return nil, BuildErrorResponse(r, err) } + defer closeBody(r) + var card *model.Card if err := json.NewDecoder(r.Body).Decode(&card); err != nil { return nil, BuildErrorResponse(r, err) @@ -450,6 +456,7 @@ func (c *Client) DeleteCategory(teamID, categoryID string) *Response { return BuildErrorResponse(r, err) } + defer closeBody(r) return BuildResponse(r) } @@ -1049,6 +1056,7 @@ func (c *Client) HideBoard(teamID, categoryID, boardID string) *Response { return BuildErrorResponse(r, err) } + defer closeBody(r) return BuildResponse(r) } @@ -1058,5 +1066,6 @@ func (c *Client) UnhideBoard(teamID, categoryID, boardID string) *Response { return BuildErrorResponse(r, err) } + defer closeBody(r) return BuildResponse(r) } diff --git a/server/services/store/sqlstore/file.go b/server/services/store/sqlstore/file.go index c1e189f3d..5825244c9 100644 --- a/server/services/store/sqlstore/file.go +++ b/server/services/store/sqlstore/file.go @@ -22,6 +22,7 @@ func (s *SQLStore) saveFileInfo(db sq.BaseRunner, fileInfo *mmModel.FileInfo) er "extension", "size", "delete_at", + "path", "archived", ). Values( @@ -31,6 +32,7 @@ func (s *SQLStore) saveFileInfo(db sq.BaseRunner, fileInfo *mmModel.FileInfo) er fileInfo.Extension, fileInfo.Size, fileInfo.DeleteAt, + fileInfo.Path, false, ) @@ -57,6 +59,7 @@ func (s *SQLStore) getFileInfo(db sq.BaseRunner, id string) (*mmModel.FileInfo, "extension", "size", "archived", + "path", ). From(s.tablePrefix + "file_info"). Where(sq.Eq{"Id": id}) @@ -73,6 +76,7 @@ func (s *SQLStore) getFileInfo(db sq.BaseRunner, id string) (*mmModel.FileInfo, &fileInfo.Extension, &fileInfo.Size, &fileInfo.Archived, + &fileInfo.Path, ) if err != nil { diff --git a/server/services/store/sqlstore/migrations/000039_add_path_to_file_info.down.sql b/server/services/store/sqlstore/migrations/000039_add_path_to_file_info.down.sql new file mode 100644 index 000000000..027b7d63f --- /dev/null +++ b/server/services/store/sqlstore/migrations/000039_add_path_to_file_info.down.sql @@ -0,0 +1 @@ +SELECT 1; \ No newline at end of file diff --git a/server/services/store/sqlstore/migrations/000039_add_path_to_file_info.up.sql b/server/services/store/sqlstore/migrations/000039_add_path_to_file_info.up.sql new file mode 100644 index 000000000..af7d5e8fe --- /dev/null +++ b/server/services/store/sqlstore/migrations/000039_add_path_to_file_info.up.sql @@ -0,0 +1 @@ +{{ addColumnIfNeeded "file_info" "path" "varchar(512)" "" }} \ No newline at end of file diff --git a/server/utils/utils.go b/server/utils/utils.go index 7a1ad9763..46326dd66 100644 --- a/server/utils/utils.go +++ b/server/utils/utils.go @@ -2,6 +2,7 @@ package utils import ( "encoding/json" + "path" "reflect" "time" @@ -120,3 +121,7 @@ func DedupeStringArr(arr []string) []string { return dedupedArr } + +func GetBaseFilePath() string { + return path.Join("boards", time.Now().Format("20060102")) +} diff --git a/webapp/src/components/table/table.scss b/webapp/src/components/table/table.scss index bbc9ef118..6d36a7215 100644 --- a/webapp/src/components/table/table.scss +++ b/webapp/src/components/table/table.scss @@ -203,6 +203,7 @@ width: inherit; } + .MultiPerson.octo-propertyvalue, .Person.octo-propertyvalue, .DateRange.octo-propertyvalue { overflow: unset; diff --git a/webapp/src/properties/date/__snapshots__/date.test.tsx.snap b/webapp/src/properties/date/__snapshots__/date.test.tsx.snap index 861e203c5..231f47e8a 100644 --- a/webapp/src/properties/date/__snapshots__/date.test.tsx.snap +++ b/webapp/src/properties/date/__snapshots__/date.test.tsx.snap @@ -34,6 +34,23 @@ exports[`properties/dateRange handle clear 1`] = ` `; +exports[`properties/dateRange returns component with new date after prop change 1`] = ` +