From 0cdad1f41d8cc2002e7c265702099dcd4e418547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Andr=C3=A9s=20V=C3=A9lez=20Vidal?= Date: Wed, 1 Feb 2023 15:30:14 +0100 Subject: [PATCH 01/56] MM-48246 - ab test open RHS with linked board; add telemetry info --- .../webapp/src/components/rhsChannelBoardItem.tsx | 13 ++++++++++++- webapp/src/telemetry/telemetryClient.ts | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx b/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx index 5f60e71a8..43ee7841d 100644 --- a/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx +++ b/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx @@ -6,6 +6,7 @@ import {FormattedMessage, useIntl} from 'react-intl' import mutator from '../../../../webapp/src/mutator' import {Utils} from '../../../../webapp/src/utils' import {getCurrentTeam} from '../../../../webapp/src/store/teams' +import {getCurrentChannel} from '../../../../webapp/src/store/channels' import {createBoard, Board} from '../../../../webapp/src/blocks/board' import {useAppSelector} from '../../../../webapp/src/store/hooks' import IconButton from '../../../../webapp/src/widgets/buttons/iconButton' @@ -17,8 +18,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) @@ -35,7 +38,15 @@ const RHSChannelBoardItem = (props: Props) => { return null } + const currentChannel = useAppSelector(getCurrentChannel) + if (!currentChannel) { + return null + } + const handleBoardClicked = (boardID: string) => { + // send the telemetry information for the clicked board + TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelsRHSBoard, {teamID: team.id, channelID: currentChannel.id}) + window.open(`${windowAny.frontendBaseURL}/team/${team.id}/${boardID}`, '_blank', 'noopener') } diff --git a/webapp/src/telemetry/telemetryClient.ts b/webapp/src/telemetry/telemetryClient.ts index 460e9d247..1787315da 100644 --- a/webapp/src/telemetry/telemetryClient.ts +++ b/webapp/src/telemetry/telemetryClient.ts @@ -50,6 +50,7 @@ export const TelemetryActions = { LimitCardLimitReached: 'limit_cardLimitReached', LimitCardLimitLinkOpen: 'limit_cardLimitLinkOpen', VersionMoreInfo: 'version_more_info', + ClickChannelsRHSBoard: 'click_channels_RHS_board', } interface IEventProps { From b4ea125a63ab10133ad3f97849c7687dd7bf5397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Andr=C3=A9s=20V=C3=A9lez=20Vidal?= Date: Wed, 1 Feb 2023 18:53:02 +0100 Subject: [PATCH 02/56] add board info --- .../webapp/src/components/rhsChannelBoardItem.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx b/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx index 43ee7841d..e796af1e5 100644 --- a/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx +++ b/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx @@ -45,7 +45,8 @@ const RHSChannelBoardItem = (props: Props) => { const handleBoardClicked = (boardID: string) => { // send the telemetry information for the clicked board - TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelsRHSBoard, {teamID: team.id, channelID: currentChannel.id}) + const extraData = {teamID: team.id, channelID: currentChannel.id, board: boardID} + TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelsRHSBoard, extraData) window.open(`${windowAny.frontendBaseURL}/team/${team.id}/${boardID}`, '_blank', 'noopener') } From 4030866a286c77ee14de1961f215a3336a9c498a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Andr=C3=A9s=20V=C3=A9lez=20Vidal?= Date: Mon, 6 Feb 2023 14:31:25 +0100 Subject: [PATCH 03/56] remove innecessary telemetry channel information --- .../webapp/src/components/rhsChannelBoardItem.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx b/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx index e796af1e5..1f03dc003 100644 --- a/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx +++ b/mattermost-plugin/webapp/src/components/rhsChannelBoardItem.tsx @@ -6,7 +6,6 @@ import {FormattedMessage, useIntl} from 'react-intl' import mutator from '../../../../webapp/src/mutator' import {Utils} from '../../../../webapp/src/utils' import {getCurrentTeam} from '../../../../webapp/src/store/teams' -import {getCurrentChannel} from '../../../../webapp/src/store/channels' import {createBoard, Board} from '../../../../webapp/src/blocks/board' import {useAppSelector} from '../../../../webapp/src/store/hooks' import IconButton from '../../../../webapp/src/widgets/buttons/iconButton' @@ -38,14 +37,9 @@ const RHSChannelBoardItem = (props: Props) => { return null } - const currentChannel = useAppSelector(getCurrentChannel) - if (!currentChannel) { - return null - } - const handleBoardClicked = (boardID: string) => { // send the telemetry information for the clicked board - const extraData = {teamID: team.id, channelID: currentChannel.id, board: boardID} + const extraData = {teamID: team.id, board: boardID} TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelsRHSBoard, extraData) window.open(`${windowAny.frontendBaseURL}/team/${team.id}/${boardID}`, '_blank', 'noopener') From fe515f6c8270b0fe396b55871efd1a02c22c0ed1 Mon Sep 17 00:00:00 2001 From: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Date: Wed, 8 Feb 2023 08:45:59 +0530 Subject: [PATCH 04/56] Fixed category menu position (#4555) --- webapp/src/components/sidebar/sidebarCategory.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/src/components/sidebar/sidebarCategory.tsx b/webapp/src/components/sidebar/sidebarCategory.tsx index c8662537e..84c19434f 100644 --- a/webapp/src/components/sidebar/sidebarCategory.tsx +++ b/webapp/src/components/sidebar/sidebarCategory.tsx @@ -313,6 +313,7 @@ const SidebarCategory = (props: Props) => { }/> { From c91a67fbe65ef12427a4d4ac5c62678f5aae1df0 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 9 Feb 2023 09:14:28 +0530 Subject: [PATCH 05/56] Used Shared resource from MM-server for attachment serving (#4542) * Used Shared resource for File Handling * Making use of shared attachment serve functionality * Added license --------- Co-authored-by: Mattermost Build --- server/api/files.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/api/files.go b/server/api/files.go index 4c2c10379..d9f6aa109 100644 --- a/server/api/files.go +++ b/server/api/files.go @@ -1,3 +1,6 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + package api import ( @@ -17,6 +20,7 @@ import ( mmModel "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/mlog" + "github.com/mattermost/mattermost-server/v6/shared/web" ) // FileUploadResponse is the response to a file upload @@ -166,7 +170,7 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { } defer fileReader.Close() - http.ServeContent(w, r, filename, time.Now(), fileReader) + web.WriteFileResponse(filename, fileInfo.MimeType, fileInfo.Size, time.Now(), "", fileReader, false, w, r) auditRec.Success() } From 5a89960b640ef9efb98df68a441b828301016a57 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 9 Feb 2023 12:50:44 +0530 Subject: [PATCH 06/56] Fixed duplicate attachment in board template (#4444) * Fixed duplicate attachment in board template * Linter fixes * Duplicate attachment fix * Code optimismed * Linter fixes * Updated test cases * update some error handling, update attachments on duplicate card * Fixed attachment section width --------- Co-authored-by: Mattermost Build Co-authored-by: Scott Bishel Co-authored-by: Harshil Sharma --- server/app/blocks.go | 42 ++++++++++++++++--- server/app/boards.go | 22 ++++++---- webapp/cypress/integration/cardURLProperty.ts | 4 -- .../src/components/cardDetail/attachment.scss | 2 +- webapp/src/mutator.ts | 3 ++ webapp/src/pages/boardPage/boardPage.tsx | 10 +++-- webapp/src/store/attachments.ts | 4 +- 7 files changed, 64 insertions(+), 23 deletions(-) diff --git a/server/app/blocks.go b/server/app/blocks.go index 9031190ed..3ed9d39d4 100644 --- a/server/app/blocks.go +++ b/server/app/blocks.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "path/filepath" + "strings" "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/notify" @@ -309,14 +310,26 @@ func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block) e for i := range copiedBlocks { block := copiedBlocks[i] + fileName := "" + isOk := false - fileName, ok := block.Fields["fileId"] - if !ok || fileName == "" { - continue // doesn't have a file attachment + switch block.Type { + case model.TypeImage: + fileName, isOk = block.Fields["fileId"].(string) + if !isOk || fileName == "" { + continue + } + case model.TypeAttachment: + fileName, isOk = block.Fields["attachmentId"].(string) + if !isOk || fileName == "" { + continue + } + default: + continue } // create unique filename in case we are copying cards within the same board. - ext := filepath.Ext(fileName.(string)) + ext := filepath.Ext(fileName) destFilename := utils.NewID(utils.IDTypeNone) + ext if destBoardID == "" || block.BoardID != destBoardID { @@ -328,7 +341,7 @@ func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block) e destTeamID = destBoard.TeamID } - sourceFilePath := filepath.Join(sourceBoard.TeamID, sourceBoard.ID, fileName.(string)) + sourceFilePath := filepath.Join(sourceBoard.TeamID, sourceBoard.ID, fileName) destinationFilePath := filepath.Join(destTeamID, block.BoardID, destFilename) a.logger.Debug( @@ -345,7 +358,24 @@ func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block) e mlog.Err(err), ) } - block.Fields["fileId"] = destFilename + if block.Type == model.TypeAttachment { + block.Fields["attachmentId"] = destFilename + parts := strings.Split(fileName, ".") + fileInfoID := parts[0][1:] + fileInfo, err := a.store.GetFileInfo(fileInfoID) + if err != nil { + return fmt.Errorf("CopyCardFiles: cannot retrieve original fileinfo: %w", err) + } + newParts := strings.Split(destFilename, ".") + newFileID := newParts[0][1:] + fileInfo.Id = newFileID + err = a.store.SaveFileInfo(fileInfo) + if err != nil { + return fmt.Errorf("CopyCardFiles: cannot create fileinfo: %w", err) + } + } else { + block.Fields["fileId"] = destFilename + } } return nil diff --git a/server/app/boards.go b/server/app/boards.go index 479926447..bb5d3ff34 100644 --- a/server/app/boards.go +++ b/server/app/boards.go @@ -202,13 +202,21 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (* blockPatches := make([]model.BlockPatch, 0) for _, block := range bab.Blocks { - if fileID, ok := block.Fields["fileId"]; ok { - blockIDs = append(blockIDs, block.ID) - blockPatches = append(blockPatches, model.BlockPatch{ - UpdatedFields: map[string]interface{}{ - "fileId": fileID, - }, - }) + fieldName := "" + if block.Type == model.TypeImage { + fieldName = "fileId" + } else if block.Type == model.TypeAttachment { + fieldName = "attachmentId" + } + if fieldName != "" { + if fieldID, ok := block.Fields[fieldName]; ok { + blockIDs = append(blockIDs, block.ID) + blockPatches = append(blockPatches, model.BlockPatch{ + UpdatedFields: map[string]interface{}{ + fieldName: fieldID, + }, + }) + } } } a.logger.Debug("Duplicate boards patching file IDs", mlog.Int("count", len(blockIDs))) diff --git a/webapp/cypress/integration/cardURLProperty.ts b/webapp/cypress/integration/cardURLProperty.ts index d6a53bba1..61fc2bc20 100644 --- a/webapp/cypress/integration/cardURLProperty.ts +++ b/webapp/cypress/integration/cardURLProperty.ts @@ -98,13 +98,9 @@ describe('Card URL Property', () => { const addView = (type: ViewType) => { cy.log(`**Add ${type} view**`) - // Intercept and wait for getUser request because it is the last one in the effects for BoardPage - // After this last request the BoardPage component will not have additional rerenders - cy.intercept('POST', '/api/v2/users').as('getUser') cy.findByRole('button', {name: 'View menu'}).click() cy.findByText('Add view').realHover() cy.findByRole('button', {name: type}).click() - cy.wait('@getUser') cy.findByRole('textbox', {name: `${type} view`}).should('exist') } diff --git a/webapp/src/components/cardDetail/attachment.scss b/webapp/src/components/cardDetail/attachment.scss index 9c70985f6..ff8902815 100644 --- a/webapp/src/components/cardDetail/attachment.scss +++ b/webapp/src/components/cardDetail/attachment.scss @@ -1,5 +1,6 @@ .Attachment { display: block; + width: 100%; .attachment-header { display: flex; @@ -13,7 +14,6 @@ padding-bottom: 20px; display: flex; overflow-x: auto; - width: 550px; } .attachment-plus-icon { diff --git a/webapp/src/mutator.ts b/webapp/src/mutator.ts index 4110c9981..3dea2ae41 100644 --- a/webapp/src/mutator.ts +++ b/webapp/src/mutator.ts @@ -12,6 +12,7 @@ import {BoardView, ISortOption, createBoardView, KanbanCalculationFields} from ' import {Card, createCard} from './blocks/card' import {ContentBlock} from './blocks/contentBlock' import {CommentBlock} from './blocks/commentBlock' +import {AttachmentBlock} from './blocks/attachmentBlock' import {FilterGroup} from './blocks/filterGroup' import octoClient from './octoClient' import undoManager from './undomanager' @@ -26,6 +27,7 @@ import store from './store' import {updateBoards} from './store/boards' import {updateViews} from './store/views' import {updateCards} from './store/cards' +import {updateAttachments} from './store/attachments' import {updateComments} from './store/comments' import {updateContents} from './store/contents' import {addBoardUsers, removeBoardUsersById} from './store/users' @@ -35,6 +37,7 @@ function updateAllBoardsAndBlocks(boards: Board[], blocks: Block[]) { store.dispatch(updateBoards(boards.filter((b: Board) => b.deleteAt !== 0) as Board[])) store.dispatch(updateViews(blocks.filter((b: Block) => b.type === 'view' || b.deleteAt !== 0) as BoardView[])) store.dispatch(updateCards(blocks.filter((b: Block) => b.type === 'card' || b.deleteAt !== 0) as Card[])) + store.dispatch(updateAttachments(blocks.filter((b: Block) => b.type === 'attachment' || b.deleteAt !== 0) as AttachmentBlock[])) store.dispatch(updateComments(blocks.filter((b: Block) => b.type === 'comment' || b.deleteAt !== 0) as CommentBlock[])) store.dispatch(updateContents(blocks.filter((b: Block) => b.type !== 'card' && b.type !== 'view' && b.type !== 'board' && b.type !== 'comment') as ContentBlock[])) }) diff --git a/webapp/src/pages/boardPage/boardPage.tsx b/webapp/src/pages/boardPage/boardPage.tsx index af3806d25..3e6bbdd44 100644 --- a/webapp/src/pages/boardPage/boardPage.tsx +++ b/webapp/src/pages/boardPage/boardPage.tsx @@ -215,13 +215,15 @@ const BoardPage = (props: Props): JSX.Element => { UserSettings.setLastViewId(match.params.boardId, viewId) } } - - if (!props.readonly && me) { - loadOrJoinBoard(me.id, teamId, match.params.boardId) - } } }, [teamId, match.params.boardId, viewId, me?.id]) + useEffect(() => { + if (match.params.boardId && !props.readonly && me) { + loadOrJoinBoard(me.id, teamId, match.params.boardId) + } + }, [teamId, match.params.boardId, me?.id]) + const handleUnhideBoard = async (boardID: string) => { if (!me || !category) { return diff --git a/webapp/src/store/attachments.ts b/webapp/src/store/attachments.ts index 918867371..879e364e3 100644 --- a/webapp/src/store/attachments.ts +++ b/webapp/src/store/attachments.ts @@ -26,7 +26,9 @@ const attachmentSlice = createSlice({ state.attachmentsByCard[attachment.parentId] = [attachment] return } - state.attachmentsByCard[attachment.parentId].push(attachment) + if (state.attachmentsByCard[attachment.parentId].findIndex((a) => a.id === attachment.id) === -1) { + state.attachmentsByCard[attachment.parentId].push(attachment) + } } else { const parentId = state.attachments[attachment.id]?.parentId if (!state.attachmentsByCard[parentId]) { From 1abd7f2645cdf3d19d348e37fba2037731f4f178 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Thu, 9 Feb 2023 18:18:32 +0500 Subject: [PATCH 07/56] GH-4478 - Forcing global header icons on the right (#4556) --- mattermost-plugin/webapp/src/plugin.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mattermost-plugin/webapp/src/plugin.scss b/mattermost-plugin/webapp/src/plugin.scss index 52c84bdc7..e71a3947c 100644 --- a/mattermost-plugin/webapp/src/plugin.scss +++ b/mattermost-plugin/webapp/src/plugin.scss @@ -2,6 +2,10 @@ font-size: 20px; } +.focalboard-body .RightControlsContainer-eacbOh { + flex-basis: auto; +} + .focalboard-body .feature-global-header>header { z-index: 1000; From f1a190d4d6bb1ababd55b3ea2628501fc859fa65 Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Sat, 11 Feb 2023 08:51:05 -0700 Subject: [PATCH 08/56] use specific class, rather than NOT an unrelated ID (#4570) --- webapp/src/widgets/menu/menu.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/widgets/menu/menu.scss b/webapp/src/widgets/menu/menu.scss index cd16b4857..9d0e2b69a 100644 --- a/webapp/src/widgets/menu/menu.scss +++ b/webapp/src/widgets/menu/menu.scss @@ -1,6 +1,6 @@ @import '../../styles/z-index'; -.Menu:not(#statusDropdownMenu) { +.Menu.noselect { @include z-index(menu); display: flex; flex-direction: column; @@ -173,7 +173,7 @@ } } -.Menu:not(#statusDropdownMenu), +.Menu.noselect, .SubMenuOption .SubMenu { @media screen and (max-width: 430px) { position: fixed; From 935e5c68fd94d7b7fcc42c5b4893a0f2ff85d5eb Mon Sep 17 00:00:00 2001 From: Doug Lauder Date: Mon, 13 Feb 2023 10:16:20 -0500 Subject: [PATCH 09/56] Use `name` column to determine if schema_migrations table is correct format (#4557) * Use name column to determine if schema_migrations table is correct format * fix result when table missing * fix result when table missing * really fix result when table missing * fix linter * more linter --------- Co-authored-by: Mattermost Build --- mattermost-plugin/server/manifest.go | 3 +- .../store/sqlstore/schema_table_migration.go | 86 ++++++++++++++----- 2 files changed, 64 insertions(+), 25 deletions(-) diff --git a/mattermost-plugin/server/manifest.go b/mattermost-plugin/server/manifest.go index db79def9f..941134c75 100644 --- a/mattermost-plugin/server/manifest.go +++ b/mattermost-plugin/server/manifest.go @@ -45,8 +45,7 @@ const manifestStr = ` "type": "bool", "help_text": "This allows board editors to share boards that can be accessed by anyone with the link.", "placeholder": "", - "default": false, - "hosting": "" + "default": false } ] } diff --git a/server/services/store/sqlstore/schema_table_migration.go b/server/services/store/sqlstore/schema_table_migration.go index cbabba697..2ab075658 100644 --- a/server/services/store/sqlstore/schema_table_migration.go +++ b/server/services/store/sqlstore/schema_table_migration.go @@ -4,11 +4,13 @@ import ( "bytes" "fmt" "io" + "strings" sq "github.com/Masterminds/squirrel" "github.com/mattermost/focalboard/server/model" - "github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/morph/models" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" ) // EnsureSchemaMigrationFormat checks the schema migrations table @@ -21,6 +23,7 @@ func (s *SQLStore) EnsureSchemaMigrationFormat() error { } if !migrationNeeded { + s.logger.Info("Schema migration table is correct format") return nil } @@ -105,8 +108,8 @@ func filterMigrations(migrations []*models.Migration, legacySchemaVersion uint32 } func (s *SQLStore) isSchemaMigrationNeeded() (bool, error) { - // Check if `dirty` column exists on schema version table. - // This column exists only for the old schema version table. + // Check if `name` column exists on schema version table. + // This column exists only for the new schema version table. // SQLite needs a bit of a special handling if s.dbType == model.SqliteDBType { @@ -114,22 +117,46 @@ func (s *SQLStore) isSchemaMigrationNeeded() (bool, error) { } query := s.getQueryBuilder(s.db). - Select("count(*)"). + Select("COLUMN_NAME"). From("information_schema.COLUMNS"). Where(sq.Eq{ - "TABLE_NAME": s.tablePrefix + "schema_migrations", - "COLUMN_NAME": "dirty", + "TABLE_NAME": s.tablePrefix + "schema_migrations", }) - row := query.QueryRow() - - var count int - if err := row.Scan(&count); err != nil { - s.logger.Error("failed to check for columns of schema_migrations table", mlog.Err(err)) + rows, err := query.Query() + if err != nil { + s.logger.Error("failed to fetch columns in schema_migrations table", mlog.Err(err)) return false, err } - return count == 1, nil + defer s.CloseRows(rows) + + data := []string{} + for rows.Next() { + var columnName string + + err := rows.Scan(&columnName) + if err != nil { + s.logger.Error("error scanning rows from schema_migrations table definition", mlog.Err(err)) + return false, err + } + + data = append(data, columnName) + } + + if len(data) == 0 { + // if no data then table does not exist and therefore a schema migration is not needed. + return false, nil + } + + for _, columnName := range data { + // look for a column named 'name', if found then no migration is needed + if strings.ToLower(columnName) == "name" { + return false, nil + } + } + + return true, nil } func (s *SQLStore) isSchemaMigrationNeededSQLite() (bool, error) { @@ -145,18 +172,27 @@ func (s *SQLStore) isSchemaMigrationNeededSQLite() (bool, error) { defer s.CloseRows(rows) + const ( + idxCid = iota + idxName + idxType + idxNotnull + idxDfltValue + idxPk + ) + data := [][]*string{} for rows.Next() { // PRAGMA returns 6 columns row := make([]*string, 6) err := rows.Scan( - &row[0], - &row[1], - &row[2], - &row[3], - &row[4], - &row[5], + &row[idxCid], + &row[idxName], + &row[idxType], + &row[idxNotnull], + &row[idxDfltValue], + &row[idxPk], ) if err != nil { s.logger.Error("error scanning rows from SQLite schema_migrations table definition", mlog.Err(err)) @@ -166,15 +202,19 @@ func (s *SQLStore) isSchemaMigrationNeededSQLite() (bool, error) { data = append(data, row) } - nameColumnFound := false + if len(data) == 0 { + // if no data then table does not exist and therefore a schema migration is not needed. + return false, nil + } + for _, row := range data { - if len(row) >= 2 && *row[1] == "dirty" { - nameColumnFound = true - break + // look for a column named 'name', if found then no migration is needed + if len(row) >= 2 && strings.ToLower(*row[idxName]) == "name" { + return false, nil } } - return nameColumnFound, nil + return true, nil } func (s *SQLStore) getLegacySchemaVersion() (uint32, error) { From 856693171a8300eeaa4ca35f59c6d2b77b832e9b Mon Sep 17 00:00:00 2001 From: Tom De Moor Date: Mon, 13 Feb 2023 16:16:30 +0100 Subject: [PATCH 10/56] Translated using Weblate (Dutch) Currently translated at 99.7% (449 of 450 strings) Translation: Focalboard/webapp Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/nl/ --- webapp/i18n/nl.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webapp/i18n/nl.json b/webapp/i18n/nl.json index e49449f89..e06b492ac 100644 --- a/webapp/i18n/nl.json +++ b/webapp/i18n/nl.json @@ -395,6 +395,10 @@ "login.log-in-button": "Aanmelden", "login.log-in-title": "Aanmelden", "login.register-button": "of maak een account aan als je er nog geen hebt", + "new_channel_modal.create_board.empty_board_description": "Maak een nieuw leeg bord", + "new_channel_modal.create_board.empty_board_title": "Leeg bord", + "new_channel_modal.create_board.select_template_placeholder": "Kies een sjabloon", + "new_channel_modal.create_board.title": "Maak een bord voor dit kanaal", "notification-box-card-limit-reached.close-tooltip": "Snooze voor 10 dagen", "notification-box-card-limit-reached.contact-link": "breng je beheerder op de hoogte", "notification-box-card-limit-reached.link": "Upgrade naar een betaald plan", From 6d6c0fc7166c52d9c8db43ef40a7db163b3b7395 Mon Sep 17 00:00:00 2001 From: master7 Date: Mon, 13 Feb 2023 16:16:30 +0100 Subject: [PATCH 11/56] Translated using Weblate (Polish) Currently translated at 100.0% (450 of 450 strings) Translation: Focalboard/webapp Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/pl/ --- webapp/i18n/pl.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webapp/i18n/pl.json b/webapp/i18n/pl.json index af21f4521..956f2dca4 100644 --- a/webapp/i18n/pl.json +++ b/webapp/i18n/pl.json @@ -395,6 +395,10 @@ "login.log-in-button": "Zaloguj się", "login.log-in-title": "Zaloguj się", "login.register-button": "lub załóż konto, jeśli jeszcze go nie masz", + "new_channel_modal.create_board.empty_board_description": "Utwórz nową pustą tablicę", + "new_channel_modal.create_board.empty_board_title": "Wyczyść tablicę", + "new_channel_modal.create_board.select_template_placeholder": "Wybierz szablon", + "new_channel_modal.create_board.title": "Utwórz tablicę dla tego kanału", "notification-box-card-limit-reached.close-tooltip": "Uśpij na 10 dni", "notification-box-card-limit-reached.contact-link": "powiadom swojego administratora", "notification-box-card-limit-reached.link": "Uaktualnienie do planu płatnego", From 137421578bc09033be37642ac6ccb201e83c74ef Mon Sep 17 00:00:00 2001 From: Matthew Williams Date: Mon, 13 Feb 2023 16:16:30 +0100 Subject: [PATCH 12/56] Translated using Weblate (English (Australia)) Currently translated at 100.0% (450 of 450 strings) Translation: Focalboard/webapp Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/en_AU/ --- webapp/i18n/en_AU.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webapp/i18n/en_AU.json b/webapp/i18n/en_AU.json index 4ba698c3b..d3f396fb4 100644 --- a/webapp/i18n/en_AU.json +++ b/webapp/i18n/en_AU.json @@ -395,6 +395,10 @@ "login.log-in-button": "Log in", "login.log-in-title": "Log in", "login.register-button": "or create an account if you don't have one", + "new_channel_modal.create_board.empty_board_description": "Create a new empty board", + "new_channel_modal.create_board.empty_board_title": "Empty board", + "new_channel_modal.create_board.select_template_placeholder": "Select a template", + "new_channel_modal.create_board.title": "Create a board for this channel", "notification-box-card-limit-reached.close-tooltip": "Snooze for 10 days", "notification-box-card-limit-reached.contact-link": "Contact your adminstrator", "notification-box-card-limit-reached.link": "Upgrade to a paid plan", From d8b82981d53401b4d6db76407a67c6591a468a1c Mon Sep 17 00:00:00 2001 From: Sergey Seroed Date: Mon, 13 Feb 2023 16:16:31 +0100 Subject: [PATCH 13/56] Translated using Weblate (Ukrainian) Currently translated at 31.5% (142 of 450 strings) Translation: Focalboard/webapp Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/uk/ Translated using Weblate (Ukrainian) Currently translated at 10.2% (46 of 450 strings) Translation: Focalboard/webapp Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/uk/ --- webapp/i18n/uk.json | 103 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/webapp/i18n/uk.json b/webapp/i18n/uk.json index 3e422bd01..390532f02 100644 --- a/webapp/i18n/uk.json +++ b/webapp/i18n/uk.json @@ -4,9 +4,9 @@ "AttachmentBlock.DeleteAction": "видалити", "AttachmentBlock.addElement": "додати {type}", "AttachmentBlock.delete": "Прикріплення успішно видалено.", - "AttachmentBlock.failed": "Неможливо завантажити файл. Досягнуто ліміт розміру прикріпленного файлу.", + "AttachmentBlock.failed": "Не вдалося завантажити цей файл, оскільки досягнуто обмеження розміру файлу.", "AttachmentBlock.upload": "Прикріплення завантажуються.", - "AttachmentBlock.uploadSuccess": "Прикріплення завантажені успішно.", + "AttachmentBlock.uploadSuccess": "Вкладення завантажено.", "AttachmentElement.delete-confirmation-dialog-button-text": "Видалити", "AttachmentElement.download": "Завантажити", "AttachmentElement.upload-percentage": "Завантаження...({uploadPercent}%)", @@ -16,13 +16,13 @@ "BoardComponent.hide": "Приховати", "BoardComponent.new": "+ Створити", "BoardComponent.no-property": "Немає {property}", - "BoardComponent.no-property-title": "Елементи з порожнім {property} полем потраплять сюди. Цей стовпець неможливо видалити.", + "BoardComponent.no-property-title": "Елементи з порожнім полем {property} потраплять сюди. Цей стовпець неможливо видалити.", "BoardComponent.show": "Показати", "BoardMember.schemeAdmin": "Адміністратор", "BoardMember.schemeCommenter": "Коментатор", "BoardMember.schemeEditor": "Редактор", "BoardMember.schemeNone": "Жоден", - "BoardMember.schemeViewer": "Глядач", + "BoardMember.schemeViewer": "Спостерігач", "BoardMember.unlinkChannel": "Від’єднати", "BoardPage.newVersion": "Доступна оновлена версія Панелі, тицьни тут щоб оновити.", "BoardPage.syncFailed": "Можливо Панель видалено або права анульовано.", @@ -44,8 +44,101 @@ "Calculations.Options.count.displayName": "Кількість", "Calculations.Options.count.label": "Кількість", "Calculations.Options.countChecked.displayName": "Перевірено", + "Calculations.Options.countChecked.label": "Кількість перевірено", + "Calculations.Options.countUnchecked.displayName": "Не перевірено", + "Calculations.Options.countUnchecked.label": "Підрахунок не перевірено", + "Calculations.Options.countUniqueValue.displayName": "Унікальний", + "Calculations.Options.countUniqueValue.label": "Підрахувати унікальні значення", + "Calculations.Options.countValue.displayName": "Значення", + "Calculations.Options.countValue.label": "Розрахунок значення", + "Calculations.Options.dateRange.displayName": "Діапазон", + "Calculations.Options.dateRange.label": "Діапазон", + "Calculations.Options.earliest.displayName": "Найраніший", + "Calculations.Options.earliest.label": "Найраніший", + "Calculations.Options.latest.displayName": "Останній", + "Calculations.Options.latest.label": "Останній", + "Calculations.Options.max.displayName": "Макс", + "Calculations.Options.max.label": "Макс", + "Calculations.Options.median.displayName": "Медіана", + "Calculations.Options.median.label": "Медіана", + "Calculations.Options.min.displayName": "Мін", + "Calculations.Options.min.label": "Мін", + "Calculations.Options.none.displayName": "Обчислити", + "Calculations.Options.none.label": "Жодного", + "Calculations.Options.percentChecked.displayName": "Перевірено", + "Calculations.Options.percentChecked.label": "Відсоток перевірено", + "Calculations.Options.percentUnchecked.displayName": "Не перевірено", + "Calculations.Options.percentUnchecked.label": "Відсоток не перевірено", + "Calculations.Options.range.displayName": "Діапазон", + "Calculations.Options.range.label": "Діапазон", + "Calculations.Options.sum.displayName": "Сума", + "Calculations.Options.sum.label": "Сума", + "CalendarCard.untitled": "Без назви", + "CardActionsMenu.copiedLink": "Скопійовано!", + "CardActionsMenu.copyLink": "Копіювати посилання", + "CardActionsMenu.delete": "Видалити", + "CardActionsMenu.duplicate": "Дублювати", + "CardBadges.title-checkboxes": "Прапорці", + "CardBadges.title-comments": "Коментарі", + "CardBadges.title-description": "Ця картка має опис", + "CardDetail.Attach": "Прикріпити", + "CardDetail.Follow": "Слідкувати", + "CardDetail.Following": "Відслідковувати", + "CardDetail.add-content": "Додайте вміст", + "CardDetail.add-icon": "Додати значок", + "CardDetail.add-property": "+ Додати властивість", + "CardDetail.addCardText": "додати текст картки", + "CardDetail.limited-body": "Перейдіть на наш план Professional або Enterprise.", + "CardDetail.limited-button": "Оновлення", + "CardDetail.limited-title": "Ця прихована картка", + "CardDetail.moveContent": "Перемістити вміст картки", + "CardDetail.new-comment-placeholder": "Додати коментар...", + "CardDetailProperty.confirm-delete-heading": "Підтвердьте видалення властивості", + "CardDetailProperty.confirm-delete-subtext": "Ви впевнені, що хочете видалити властивість \"{propertyName}\"? При видаленні властивість буде видалено з усіх карток на цій дошці.", + "CardDetailProperty.confirm-property-name-change-subtext": "Ви дійсно хочете змінити властивість \"{propertyName}\" {customText}? Це вплине на значення(-я) на {numOfCards} картці(-ах) на цій дошці і може призвести до втрати даних.", + "CardDetailProperty.confirm-property-type-change": "Підтвердити зміну типу власності", + "CardDetailProperty.delete-action-button": "Видалити", + "CardDetailProperty.property-change-action-button": "Змінити властивість", + "CardDetailProperty.property-changed": "Властивість змінена успішно!", + "CardDetailProperty.property-deleted": "{propertyName} успішно видалено!", + "CardDetailProperty.property-name-change-subtext": "тип з \"{oldPropType}\" в \"{newPropType}\"", + "CardDetial.limited-link": "Дізнайтеся більше про наші плани.", + "CardDialog.delete-confirmation-dialog-attachment": "Підтвердити видалення вкладення", + "CardDialog.delete-confirmation-dialog-button-text": "Видалити", + "CardDialog.delete-confirmation-dialog-heading": "Підтвердити видалення картки", + "CardDialog.editing-template": "Ви редагуєте шаблон.", + "CardDialog.nocard": "Ця картка не існує або недоступна.", + "Categories.CreateCategoryDialog.CancelText": "Скасувати", + "Categories.CreateCategoryDialog.CreateText": "Створити", + "Categories.CreateCategoryDialog.Placeholder": "Назвіть свою категорію", + "Categories.CreateCategoryDialog.UpdateText": "Оновити", + "CenterPanel.Login": "Логін", + "CenterPanel.Share": "Поділитися", + "ChannelIntro.CreateBoard": "Створити дошку", + "CloudMessage.cloud-server": "Отримайте власний безкоштовний хмарний сервер.", + "ColorOption.selectColor": "Виберіть колір {color}", + "Comment.delete": "Видалити", + "CommentsList.send": "Надіслати", + "ConfirmPerson.empty": "Порожній", + "ConfirmPerson.search": "Пошук...", + "ConfirmationDialog.cancel-action": "Скасувати", + "ConfirmationDialog.confirm-action": "Підтвердити", + "ContentBlock.Delete": "Видалити", + "ContentBlock.DeleteAction": "видалити", + "DeleteBoardDialog.confirm-delete": "Видалити", + "FilterComponent.delete": "Видалити", + "PropertyMenu.Delete": "Видалити", + "Sidebar.delete-board": "Видалити дошку", + "SidebarCategories.CategoryMenu.Delete": "Видалити категорію", + "SidebarCategories.CategoryMenu.DeleteModal.Title": "Видалити дану категорію?", + "TableHeaderMenu.delete": "Видалити", + "View.DeleteView": "Видалити вид", + "ViewHeader.delete-template": "Видалити", "generic.previous": "Попередній", - "tutorial_tip.ok": "Гаразд", + "shareBoard.unknown-channel-display-name": "Невідомий канал", + "tutorial_tip.finish_tour": "Готово", + "tutorial_tip.got_it": "Зрозуміло", + "tutorial_tip.ok": "Далі", "tutorial_tip.out": "Відмовтеся від цих порад.", "tutorial_tip.seen": "Ви бачили це раніше?" } From 7d468879326801c3d119e5450722723ce1444fe3 Mon Sep 17 00:00:00 2001 From: MArtin Johnson Date: Mon, 13 Feb 2023 16:16:31 +0100 Subject: [PATCH 14/56] Translated using Weblate (Swedish) Currently translated at 100.0% (450 of 450 strings) Translation: Focalboard/webapp Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/sv/ --- webapp/i18n/sv.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webapp/i18n/sv.json b/webapp/i18n/sv.json index ff99e1b67..6cf67205d 100644 --- a/webapp/i18n/sv.json +++ b/webapp/i18n/sv.json @@ -395,6 +395,10 @@ "login.log-in-button": "Logga in", "login.log-in-title": "Logga in", "login.register-button": "eller skapa ett konto om du inte redan har ett", + "new_channel_modal.create_board.empty_board_description": "Skapa en ny tom tavla", + "new_channel_modal.create_board.empty_board_title": "Tom tavla", + "new_channel_modal.create_board.select_template_placeholder": "Välj en mall", + "new_channel_modal.create_board.title": "Skapa en tavla för den här kanalen", "notification-box-card-limit-reached.close-tooltip": "Sov i 10 dagar", "notification-box-card-limit-reached.contact-link": "notifiera din administratör", "notification-box-card-limit-reached.link": "Uppgradera till ett betal-abonnemang", From fdc1ece02a842cc669ae4cb0cc457a4463cc0f34 Mon Sep 17 00:00:00 2001 From: Rizumu85 Date: Mon, 13 Feb 2023 16:16:31 +0100 Subject: [PATCH 15/56] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (450 of 450 strings) Translation: Focalboard/webapp Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/zh_Hans/ --- webapp/i18n/zh_Hans.json | 167 ++++++++++++++++++++++++++++++++++----- 1 file changed, 146 insertions(+), 21 deletions(-) diff --git a/webapp/i18n/zh_Hans.json b/webapp/i18n/zh_Hans.json index cfb6c8c91..5e176cf61 100644 --- a/webapp/i18n/zh_Hans.json +++ b/webapp/i18n/zh_Hans.json @@ -3,10 +3,10 @@ "Attachment.Attachment-title": "附件", "AttachmentBlock.DeleteAction": "删除", "AttachmentBlock.addElement": "添加 {type}", - "AttachmentBlock.delete": "附件已删除", + "AttachmentBlock.delete": "附件已删除。", "AttachmentBlock.failed": "该文件无法上传,因为已经达到了文件大小的限制。", "AttachmentBlock.upload": "附件正在上传。", - "AttachmentBlock.uploadSuccess": "附件上传成功。", + "AttachmentBlock.uploadSuccess": "附件已上传。", "AttachmentElement.delete-confirmation-dialog-button-text": "删除", "AttachmentElement.download": "下载", "AttachmentElement.upload-percentage": "上传中…({uploadPercent}%)", @@ -16,7 +16,7 @@ "BoardComponent.hide": "隐藏", "BoardComponent.new": "+ 新增", "BoardComponent.no-property": "无 {property}", - "BoardComponent.no-property-title": "{property} 属性为空的项目将转到此处。该列无法删除。", + "BoardComponent.no-property-title": "{property} 属性为空的项目将转到此处,该列无法删除。", "BoardComponent.show": "显示", "BoardMember.schemeAdmin": "管理", "BoardMember.schemeCommenter": "评论者", @@ -27,7 +27,7 @@ "BoardPage.newVersion": "Boards 的新版本已可用,点击这里重新加载。", "BoardPage.syncFailed": "板块或许已被删除或访问授权已被撤销。", "BoardTemplateSelector.add-template": "创建新模板", - "BoardTemplateSelector.create-empty-board": "创建空白看板", + "BoardTemplateSelector.create-empty-board": "创建空白板块", "BoardTemplateSelector.delete-template": "删除", "BoardTemplateSelector.description": "选择一个模板助你开始。或者创建一个空白板块,从零开始。", "BoardTemplateSelector.edit-template": "编辑", @@ -35,7 +35,7 @@ "BoardTemplateSelector.plugin.no-content-title": "创建一个看板", "BoardTemplateSelector.title": "创建一个看板", "BoardTemplateSelector.use-this-template": "使用该模板", - "BoardsSwitcher.Title": "查找看板", + "BoardsSwitcher.Title": "查找板块", "BoardsUnfurl.Limited": "由于卡片被存档,其他细节被隐藏", "BoardsUnfurl.Remainder": "+{remainder} 更多", "BoardsUnfurl.Updated": "于 {time} 更新", @@ -103,9 +103,9 @@ "CardDetailProperty.property-deleted": "成功删除 {propertyName}!", "CardDetailProperty.property-name-change-subtext": "属性的类型从\"{oldPropType}\" 更改为\"{newPropType}\"", "CardDetial.limited-link": "了解更多关于我们的计划。", - "CardDialog.delete-confirmation-dialog-attachment": "确认删除附件!", + "CardDialog.delete-confirmation-dialog-attachment": "确认删除附件", "CardDialog.delete-confirmation-dialog-button-text": "删除", - "CardDialog.delete-confirmation-dialog-heading": "确认删除卡片!", + "CardDialog.delete-confirmation-dialog-heading": "确认删除卡片", "CardDialog.editing-template": "您正在编辑模板。", "CardDialog.nocard": "卡片不存在或者无法被存取。", "Categories.CreateCategoryDialog.CancelText": "取消", @@ -161,10 +161,15 @@ "Filter.is-not-set": "未设置", "Filter.is-set": "被设定为", "Filter.not-contains": "不包含", - "Filter.not-ends-with": "并不以", - "Filter.not-includes": "不包含", + "Filter.not-ends-with": "不结束于", + "Filter.not-includes": "不含有", + "Filter.not-starts-with": "不开始于", + "Filter.starts-with": "开始于", + "FilterByText.placeholder": "过滤文本", "FilterComponent.add-filter": "+ 增加过滤条件", "FilterComponent.delete": "删除", + "FilterValue.empty": "(空)", + "FindBoardsDialog.IntroText": "搜索板块", "FindBoardsDialog.NoResultsFor": "没有\"{searchQuery}\"相关的结果", "FindBoardsDialog.NoResultsSubtext": "请检查拼写或者查找其他内容。", "FindBoardsDialog.SubTitle": "输入内容来查找板块。使用上/下浏览。ENTER选择,ESC取消", @@ -172,20 +177,29 @@ "GroupBy.hideEmptyGroups": "隐藏{count}个空组", "GroupBy.showHiddenGroups": "显示已隐藏的{count}个组", "GroupBy.ungroup": "未分组", + "HideBoard.MenuOption": "隐藏板块", "KanbanCard.untitled": "无标题", + "MentionSuggestion.is-not-board-member": "(非板块成员)", + "Mutator.new-board-from-template": "从模板创建板块", "Mutator.new-card-from-template": "使用模板新增卡片", "Mutator.new-template-from-card": "从卡片新增模板", "OnboardingTour.AddComments.Body": "你可以对问题进行评论,甚至可以@提及你的Mattermost同伴,以引起他们的注意。", "OnboardingTour.AddComments.Title": "添加评论", "OnboardingTour.AddDescription.Body": "在你的卡片上添加描述,以便其他人了解卡片的内容。", "OnboardingTour.AddDescription.Title": "添加描述", - "OnboardingTour.AddProperties.Body": "为卡片添加各种属性,使其更加强大!", + "OnboardingTour.AddProperties.Body": "为卡片添加各种属性,使其更加强大。", "OnboardingTour.AddProperties.Title": "添加属性", "OnboardingTour.AddView.Body": "在这里创建一个新的视图,用不同的布局来组织你的板块。", "OnboardingTour.AddView.Title": "添加一个新的视图", + "OnboardingTour.CopyLink.Body": "你可以通过频道,私信和群聊分享链接来和成员们一起共享卡片。", "OnboardingTour.CopyLink.Title": "复制链接", + "OnboardingTour.OpenACard.Body": "打开卡片来探索板块的高效使用方法,从而助力你的整理项目。", "OnboardingTour.OpenACard.Title": "打开一个卡片", + "OnboardingTour.ShareBoard.Body": "你可以分享板块,不管是与内部成员,还是公开发布到外部的机构。", "OnboardingTour.ShareBoard.Title": "分享板块", + "PersonProperty.board-members": "板块成员", + "PersonProperty.me": "我", + "PersonProperty.non-board-members": "非板块成员", "PropertyMenu.Delete": "删除", "PropertyMenu.changeType": "修改属性类型", "PropertyMenu.selectType": "选择属性类型", @@ -195,14 +209,17 @@ "PropertyType.CreatedTime": "创建时间", "PropertyType.Date": "日期", "PropertyType.Email": "Email", + "PropertyType.MultiPerson": "多人", "PropertyType.MultiSelect": "多选", "PropertyType.Number": "数字", "PropertyType.Person": "个人", "PropertyType.Phone": "电话号码", "PropertyType.Select": "选取", "PropertyType.Text": "文字框", + "PropertyType.Unknown": "未知", "PropertyType.UpdatedBy": "最后更新者", "PropertyType.UpdatedTime": "上次更新时间", + "PropertyType.Url": "URL", "PropertyValueElement.empty": "空的", "RegistrationLink.confirmRegenerateToken": "此动作将使先前分享的链接无效。确定要进行吗?", "RegistrationLink.copiedLink": "已复制!", @@ -210,20 +227,22 @@ "RegistrationLink.description": "将此链接分享给他人以建立帐号:", "RegistrationLink.regenerateToken": "重新生成令牌", "RegistrationLink.tokenRegenerated": "已重新生成注册链接", - "ShareBoard.PublishDescription": "发布并与所有人分享 \"只读 \"链接", + "ShareBoard.PublishDescription": "发布并与所有人分享 \"只读 \"链接。", "ShareBoard.PublishTitle": "发布到网上", "ShareBoard.ShareInternal": "内部分享", - "ShareBoard.ShareInternalDescription": "有权限的用户将能够使用这个链接", + "ShareBoard.ShareInternalDescription": "有权限的用户将能够使用这个链接。", "ShareBoard.Title": "分享板块", "ShareBoard.confirmRegenerateToken": "此动作将使先前分享的链接无效。确定要进行吗?", "ShareBoard.copiedLink": "已复制!", "ShareBoard.copyLink": "复制链接", "ShareBoard.regenerate": "重新生成令牌", + "ShareBoard.searchPlaceholder": "搜索成员和频道", "ShareBoard.teamPermissionsText": "在{teamName}团队的每个人", "ShareBoard.tokenRegenrated": "已重新产生令牌", "ShareBoard.userPermissionsRemoveMemberText": "移除成员", "ShareBoard.userPermissionsYouText": "(你)", "ShareTemplate.Title": "分享模板", + "ShareTemplate.searchPlaceholder": "搜索成员", "Sidebar.about": "关于 Focalboard", "Sidebar.add-board": "+ 新增版面", "Sidebar.changePassword": "变更密码", @@ -234,18 +253,32 @@ "Sidebar.import-archive": "导入档案", "Sidebar.invite-users": "邀请使用者", "Sidebar.logout": "登出", + "Sidebar.new-category.badge": "新建", + "Sidebar.new-category.drag-boards-cta": "拖动板块到这里...", "Sidebar.no-boards-in-category": "里面没有板块", + "Sidebar.product-tour": "产品导览", "Sidebar.random-icons": "随机图标", "Sidebar.set-language": "设定语言", "Sidebar.set-theme": "设置主题", "Sidebar.settings": "设定", "Sidebar.template-from-board": "从板块新增一个模板", "Sidebar.untitled-board": "(无标题版面)", + "Sidebar.untitled-view": "(未命名视图)", "SidebarCategories.BlocksMenu.Move": "移动到...", "SidebarCategories.CategoryMenu.CreateNew": "创建新类别", "SidebarCategories.CategoryMenu.Delete": "删除类别", + "SidebarCategories.CategoryMenu.DeleteModal.Body": "在于{categoryName}的板块会被移回板块类别。这并不会移除任何板块。", "SidebarCategories.CategoryMenu.DeleteModal.Title": "删除此类别?", "SidebarCategories.CategoryMenu.Update": "重命名类别", + "SidebarTour.ManageCategories.Body": "新建并管理自定义的类别。类别是用户专属的,所以移动板块到你的类别不会影响到使用同个板块的其他成员。", + "SidebarTour.ManageCategories.Title": "管理类别", + "SidebarTour.SearchForBoards.Body": "打开类别切换器(Cmd/Ctrl+K)来快速查找并添加板块到你的侧边栏。", + "SidebarTour.SearchForBoards.Title": "搜索板块", + "SidebarTour.SidebarCategories.Body": "你所有的板块现会在侧边栏下被管理。无需在不同工作区中进行切换。基于你之前工作区的一次性自定义板块,将会作为v7.2版本更新自动创建。这个特性可以在设置里更改会或移除。", + "SidebarTour.SidebarCategories.Link": "了解更多", + "SidebarTour.SidebarCategories.Title": "侧边栏类别", + "SiteStats.total_boards": "所有板块", + "SiteStats.total_cards": "所有卡片", "TableComponent.add-icon": "加入图标", "TableComponent.name": "姓名", "TableComponent.plus-new": "+ 新增", @@ -256,14 +289,23 @@ "TableHeaderMenu.insert-right": "在右侧插入", "TableHeaderMenu.sort-ascending": "升序排列", "TableHeaderMenu.sort-descending": "降序排列", + "TableRow.DuplicateCard": "复制卡片", + "TableRow.MoreOption": "更多操作", "TableRow.open": "开启", "TopBar.give-feedback": "反馈问题", "URLProperty.copiedLink": "已复制!", "URLProperty.copy": "复制", "URLProperty.edit": "编辑", + "UndoRedoHotKeys.canRedo": "撤回", + "UndoRedoHotKeys.canRedo-with-description": "撤回 {description}", + "UndoRedoHotKeys.canUndo": "撤销", + "UndoRedoHotKeys.canUndo-with-description": "撤销 {description}", + "UndoRedoHotKeys.cannotRedo": "已没有操作可撤回", + "UndoRedoHotKeys.cannotUndo": "已没有操作可撤销", "ValueSelector.noOptions": "没有选项。现在添加一个!", "ValueSelector.valueSelector": "值选择器", "ValueSelectorLabel.openMenu": "打开菜单", + "VersionMessage.help": "了解查看新版本有什么新特性。", "View.AddView": "添加视图", "View.Board": "板块", "View.DeleteView": "删除视图", @@ -273,6 +315,8 @@ "View.NewCalendarTitle": "日历视图", "View.NewGalleryTitle": "画廊视图", "View.NewTableTitle": "图表视图", + "View.NewTemplateDefaultTitle": "未命名模板", + "View.NewTemplateTitle": "未命名", "View.Table": "图表", "ViewHeader.add-template": "+ 新模板", "ViewHeader.delete-template": "删除", @@ -288,40 +332,121 @@ "ViewHeader.new": "新", "ViewHeader.properties": "属性", "ViewHeader.properties-menu": "属性菜单", - "ViewHeader.search-text": "搜索文本", + "ViewHeader.search-text": "搜索卡片", "ViewHeader.select-a-template": "选择范本", "ViewHeader.set-default-template": "设为默认范本", "ViewHeader.sort": "排序", "ViewHeader.untitled": "无标题", + "ViewHeader.view-header-menu": "查看标题菜单", + "ViewHeader.view-menu": "查看菜单", + "ViewLimitDialog.Heading": "已达到板块观看的限制", + "ViewLimitDialog.PrimaryButton.Title.Admin": "升级", + "ViewLimitDialog.PrimaryButton.Title.RegularUser": "通知管理员", + "ViewLimitDialog.Subtext.Admin": "升级到专业版或企业版。", + "ViewLimitDialog.Subtext.Admin.PricingPageLink": "了解更多关于我们的付费套装。", + "ViewLimitDialog.Subtext.RegularUser": "通知你的管理员来升级到专业版和企业版。", + "ViewLimitDialog.UpgradeImg.AltText": "升级图片", + "ViewLimitDialog.notifyAdmin.Success": "已通知管理员", "ViewTitle.hide-description": "隐藏描述", "ViewTitle.pick-icon": "挑选图标", "ViewTitle.random-icon": "随机", "ViewTitle.remove-icon": "移除图标", "ViewTitle.show-description": "显示描述", - "ViewTitle.untitled-board": "无标题版面", - "WelcomePage.Description": "Boards 是一个项目管理工具,使用熟悉的看板视图,帮助你的团队策划、组织、跟踪和管理跨团队的工作", - "WelcomePage.Heading": "欢迎来到看板", + "ViewTitle.untitled-board": "无标题板块", + "WelcomePage.Description": "板块是一个项目管理工具,使用熟悉的看板视图,帮助你的团队策划、组织、跟踪和管理跨团队的工作。", + "WelcomePage.Explore.Button": "探索", + "WelcomePage.Heading": "欢迎来到板块", "WelcomePage.NoThanks.Text": "不了,请让我自己设置", + "WelcomePage.StartUsingIt.Text": "开始使用", "Workspace.editing-board-template": "您正在编辑版面模板。", + "badge.guest": "访客", + "boardSelector.confirm-link-board": "连接板块到频道", + "boardSelector.confirm-link-board-button": "是的,连接板块", + "boardSelector.confirm-link-board-subtext": "当你连接“{boardName}”到频道时,所有频道的成员(现有的或新的)都可以编辑。这并不包括访客。你随时都可以取消板块与频道的连接。", + "boardSelector.confirm-link-board-subtext-with-other-channel": "当你连接\"{boardName}\"到频道时,此频道的所有成员(现有的和新的)将可以进行编辑,这并不包括访客。{lineBreak} 此板块目前与另一个频道已有连接,如果在此进行新的连接,那么将会自动取消之前连接的频道。", + "boardSelector.create-a-board": "创建板块", + "boardSelector.link": "连接", + "boardSelector.search-for-boards": "搜索板块", + "boardSelector.title": "连接板块", + "boardSelector.unlink": "取消连接", "calendar.month": "月", "calendar.today": "今天", "calendar.week": "周", + "centerPanel.undefined": "不{propertyName}", + "centerPanel.unknown-user": "陌生用户", + "cloudMessage.learn-more": "了解更多", "createImageBlock.failed": "图片上传失败,超过大小限制。", "default-properties.badges": "评论和描述", "default-properties.title": "标题", - "error.page.title": "抱歉,出错了", + "error.back-to-home": "回到主页", + "error.back-to-team": "回到团队", + "error.board-not-found": "未找到板块。", + "error.go-login": "登陆", + "error.invalid-read-only-board": "你没有权限访问此板块,请登陆后再进行访问板块。", + "error.not-logged-in": "尚未登陆或会话超时,请登陆后再进行访问板块。", + "error.page.title": "抱歉,出现了一些错误", + "error.team-undefined": "不是有效的团队。", + "error.unknown": "发生了一些错误。", "generic.previous": "上一个", - "imagePaste.upload-failed": "图片上传失败,超过大小限制", + "guest-no-board.subtitle": "你尚未有权限访问此团队的任何一个板块,请等待某人把你添加到某个板块。", + "guest-no-board.title": "尚未有板块", + "imagePaste.upload-failed": "图片上传失败,超过大小限制。", + "limitedCard.title": "卡片已隐藏", "login.log-in-button": "登录", "login.log-in-title": "登录", "login.register-button": "或创建一个帐户(如果您没有帐户)", + "new_channel_modal.create_board.empty_board_description": "创建一个空白的板块", + "new_channel_modal.create_board.empty_board_title": "空白板块", + "new_channel_modal.create_board.select_template_placeholder": "选择模板", + "new_channel_modal.create_board.title": "为此频道创建一个新板块", + "notification-box-card-limit-reached.close-tooltip": "小睡十天", + "notification-box-card-limit-reached.contact-link": "通知你的管理员", + "notification-box-card-limit-reached.link": "升级到付费版", + "notification-box-card-limit-reached.title": "板块上的{cards}卡片已隐藏", + "notification-box-cards-hidden.title": "此行动已隐藏其他卡片", + "notification-box.card-limit-reached.not-admin.text": "要访问存档的卡片,你需要通过 {contactLink} 来升级到付费版。", + "notification-box.card-limit-reached.text": "已达到卡片上限,如需查看旧卡片请点{link}", + "person.add-user-to-board": "将 {username} 加入板块", + "person.add-user-to-board-confirm-button": "添加到板块", + "person.add-user-to-board-permissions": "权限", + "person.add-user-to-board-question": "你想将 {username} 加入板块吗?", + "person.add-user-to-board-warning": "{username} 不是此板块的成员,因此不会受到任何关于此板块的通知。", "register.login-button": "或登录(如果您已拥有帐户)", "register.signup-title": "注册您的帐户", + "rhs-board-non-admin-msg": "你不是板块的管理员", + "rhs-boards.add": "添加", + "rhs-boards.dm": "私信", + "rhs-boards.gm": "群聊", + "rhs-boards.header.dm": "此私信", + "rhs-boards.header.gm": "此群聊信息", + "rhs-boards.last-update-at": "最后更新日为:{datetime}", + "rhs-boards.link-boards-to-channel": "把板块连接到 {channelName}", + "rhs-boards.linked-boards": "连接板块", + "rhs-boards.no-boards-linked-to-channel": "尚未有板块与{channelName} 连接", + "rhs-boards.no-boards-linked-to-channel-description": "板块是一个能帮助我们定义,组织,追踪和管理团队工作的一个专业管理工具,可通过使用熟悉的看板视图。", + "rhs-boards.unlink-board": "取消连接板块", + "rhs-boards.unlink-board1": "取消连接板块", + "rhs-channel-boards-header.title": "板块", "share-board.publish": "发布", "share-board.share": "分享", - "shareBoard.lastAdmin": "Boards 至少得有一位管理员", + "shareBoard.channels-select-group": "频道", + "shareBoard.confirm-change-team-role.body": "此板块低于“{role}”的所有人都将于现在被提升到{role}。你确认要更改此板块的最低职责?", + "shareBoard.confirm-change-team-role.confirmBtnText": "更改板块的最低职责", + "shareBoard.confirm-change-team-role.title": "更改板块的最低职责", + "shareBoard.confirm-link-channel": "连接板块到频道", + "shareBoard.confirm-link-channel-button": "连接频道", + "shareBoard.confirm-link-channel-button-with-other-channel": "再此取消或进行连接", + "shareBoard.confirm-link-channel-subtext": "当你把频道连接到一个板块,此频道里的所有成员(现有的或新的)都可以进行编辑,这并不包括访客。", + "shareBoard.confirm-link-channel-subtext-with-other-channel": "当你把频道连接到一个板块,此频道里的所有成员(现有的或新的)都可以进行编辑,这并不包括访客。{lineBreak}此板块目前与另一个频道已有连接,如果在此进行新的连接,那么将会自动取消之前连接的频道。", + "shareBoard.confirm-unlink.body": "当你取消频道与板块的连接,频道的所有成员(现有的或新的)将会失去板块的访问权限,除非单独给予许可。", + "shareBoard.confirm-unlink.confirmBtnText": "取消连接频道", + "shareBoard.confirm-unlink.title": "取消连接此板块的频道", + "shareBoard.lastAdmin": "板块至少得有一位管理员", + "shareBoard.members-select-group": "成员", + "shareBoard.unknown-channel-display-name": "未知频道", "tutorial_tip.finish_tour": "完成", - "tutorial_tip.got_it": "明白了", + "tutorial_tip.got_it": "了解", "tutorial_tip.ok": "下一个", - "tutorial_tip.seen": "之前见过这吗?" + "tutorial_tip.out": "选择不使用这些提示。", + "tutorial_tip.seen": "之前有见到过吗?" } From 9efea6ac3bc4bd217baf2408e9992a5f0578b3cf Mon Sep 17 00:00:00 2001 From: kaakaa Date: Mon, 13 Feb 2023 16:16:31 +0100 Subject: [PATCH 16/56] Translated using Weblate (Japanese) Currently translated at 100.0% (450 of 450 strings) Translation: Focalboard/webapp Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/ja/ --- webapp/i18n/ja.json | 158 +++++++++++++++++++++++--------------------- 1 file changed, 81 insertions(+), 77 deletions(-) diff --git a/webapp/i18n/ja.json b/webapp/i18n/ja.json index c0299f2ad..695793325 100644 --- a/webapp/i18n/ja.json +++ b/webapp/i18n/ja.json @@ -1,5 +1,5 @@ { - "AppBar.Tooltip": "リンク先ボードの表示切り替え", + "AppBar.Tooltip": "リンク先Boardの切替え", "Attachment.Attachment-title": "添付する", "AttachmentBlock.DeleteAction": "削除", "AttachmentBlock.addElement": "{type} を追加", @@ -24,18 +24,18 @@ "BoardMember.schemeNone": "なし", "BoardMember.schemeViewer": "閲覧者", "BoardMember.unlinkChannel": "リンク解除", - "BoardPage.newVersion": "ボードの新しいバージョンが利用可能です。ここをクリックして再読み込みしてください。", - "BoardPage.syncFailed": "ボードが削除されたか、アクセスが取り消されました。", + "BoardPage.newVersion": "Boardsの新しいバージョンが利用可能です。ここをクリックして再読み込みしてください。", + "BoardPage.syncFailed": "Boardが削除されたか、アクセスが取り消されました。", "BoardTemplateSelector.add-template": "テンプレート新規作成", - "BoardTemplateSelector.create-empty-board": "空のボードを作成", + "BoardTemplateSelector.create-empty-board": "空のBoardを作成", "BoardTemplateSelector.delete-template": "削除する", - "BoardTemplateSelector.description": "以下のテンプレートを使用するか、空の状態から作成することで、サイドバーにボードを追加できます。", + "BoardTemplateSelector.description": "以下のテンプレートを使用するか、空の状態から作成することで、サイドバーにBoardを追加できます。", "BoardTemplateSelector.edit-template": "編集", - "BoardTemplateSelector.plugin.no-content-description": "以下のテンプレートを使用するか、空の状態から作成することで、サイドバーにボードを追加できます。", - "BoardTemplateSelector.plugin.no-content-title": "ボードを作成する", - "BoardTemplateSelector.title": "ボードを作成する", + "BoardTemplateSelector.plugin.no-content-description": "以下のテンプレートを使用するか、空の状態から作成することで、サイドバーにBoardを追加できます。", + "BoardTemplateSelector.plugin.no-content-title": "Boardを作成する", + "BoardTemplateSelector.title": "Boardを作成する", "BoardTemplateSelector.use-this-template": "このテンプレートを使う", - "BoardsSwitcher.Title": "ボード検索", + "BoardsSwitcher.Title": "Board検索", "BoardsUnfurl.Limited": "カードがアーカイブされているため詳細は表示されません", "BoardsUnfurl.Remainder": "残り +{remainder}", "BoardsUnfurl.Updated": "更新日時 {time}", @@ -94,8 +94,8 @@ "CardDetail.moveContent": "カード内容の移動", "CardDetail.new-comment-placeholder": "コメントを追加する...", "CardDetailProperty.confirm-delete-heading": "プロパティの削除を確定する", - "CardDetailProperty.confirm-delete-subtext": "本当にプロパティ \"{propertyName}\" を削除しますか? 削除すると、このボードのすべてのカードからそのプロパティが削除されます。", - "CardDetailProperty.confirm-property-name-change-subtext": "本当にプロパティ \"{propertyName}\" の \"{customText}\" に変更しますか? これは、このボードの{numOfCards}カード全体の値に影響し、データの損失につながる恐れがあります。", + "CardDetailProperty.confirm-delete-subtext": "本当にプロパティ \"{propertyName}\" を削除しますか? 削除すると、このBoardのすべてのカードからそのプロパティが削除されます。", + "CardDetailProperty.confirm-property-name-change-subtext": "本当にプロパティ \"{propertyName}\" の \"{customText}\" に変更しますか? これは、このBoardの{numOfCards}カード全体の値に影響し、データの損失につながる恐れがあります。", "CardDetailProperty.confirm-property-type-change": "プロパティ種別の変更を確定する", "CardDetailProperty.delete-action-button": "削除", "CardDetailProperty.property-change-action-button": "プロパティの変更", @@ -114,7 +114,7 @@ "Categories.CreateCategoryDialog.UpdateText": "更新", "CenterPanel.Login": "ログイン", "CenterPanel.Share": "共有", - "ChannelIntro.CreateBoard": "ボードを作成する", + "ChannelIntro.CreateBoard": "Boardを作成する", "CloudMessage.cloud-server": "専用の無料クラウドサーバーを入手する。", "ColorOption.selectColor": "{color} 色を選択", "Comment.delete": "削除", @@ -144,10 +144,10 @@ "DateRange.today": "今日", "DeleteBoardDialog.confirm-cancel": "キャンセル", "DeleteBoardDialog.confirm-delete": "削除", - "DeleteBoardDialog.confirm-info": "本当にボード \"{boardTitle}\" を削除しますか? 削除すると、このボードのすべてのカードが削除されます。", - "DeleteBoardDialog.confirm-info-template": "ボードテンプレート \"{boardTitle}\" を本当に削除しますか?", - "DeleteBoardDialog.confirm-tite": "ボードの削除を確定する", - "DeleteBoardDialog.confirm-tite-template": "ボードテンプレートの削除を確定する", + "DeleteBoardDialog.confirm-info": "本当にBoard \"{boardTitle}\" を削除しますか? 削除すると、このBoardのすべてのカードが削除されます。", + "DeleteBoardDialog.confirm-info-template": "Boardテンプレート \"{boardTitle}\" を本当に削除しますか?", + "DeleteBoardDialog.confirm-tite": "Boardの削除を確定する", + "DeleteBoardDialog.confirm-tite-template": "Boardテンプレートの削除を確定する", "Dialog.closeDialog": "ダイアログを閉じる", "EditableDayPicker.today": "今日", "Error.mobileweb": "モバイルウェブのサポートは現在、初期ベータ版です。一部の機能が利用できない場合があります。", @@ -169,18 +169,18 @@ "FilterComponent.add-filter": "+ フィルターを追加する", "FilterComponent.delete": "削除", "FilterValue.empty": "(空)", - "FindBoardsDialog.IntroText": "ボードを検索", + "FindBoardsDialog.IntroText": "Boardを検索", "FindBoardsDialog.NoResultsFor": "\"{searchQuery}\"に対する結果はありません", "FindBoardsDialog.NoResultsSubtext": "スペルを確認し、再度検索してください。", - "FindBoardsDialog.SubTitle": "ボードを検索するために文字を入力してください。UP/DOWNで閲覧、ENTERで選択、ESCでキャンセル", - "FindBoardsDialog.Title": "ボードを探す", + "FindBoardsDialog.SubTitle": "Boardを検索するために文字を入力してください。UP/DOWNで閲覧、ENTERで選択、ESCでキャンセル", + "FindBoardsDialog.Title": "Boardを探す", "GroupBy.hideEmptyGroups": "{count} 個の空のグループを隠す", "GroupBy.showHiddenGroups": "{count} 個の非表示グループを表示する", "GroupBy.ungroup": "グループ解除", - "HideBoard.MenuOption": "ボードを隠す", + "HideBoard.MenuOption": "Boardを隠す", "KanbanCard.untitled": "無題", "MentionSuggestion.is-not-board-member": "(not board member)", - "Mutator.new-board-from-template": "テンプレートからの新しいボード", + "Mutator.new-board-from-template": "テンプレートからの新しいBoard", "Mutator.new-card-from-template": "テンプレートから新しいカードを作成", "Mutator.new-template-from-card": "カードから新しいテンプレートを作成", "OnboardingTour.AddComments.Body": "問題にコメントしたり、仲間のMattermostユーザーの注意を引くために@メンションすることもできます。", @@ -189,14 +189,14 @@ "OnboardingTour.AddDescription.Title": "説明を追加する", "OnboardingTour.AddProperties.Body": "カードに様々なプロパティを追加することで、より便利になります。", "OnboardingTour.AddProperties.Title": "プロパティを追加する", - "OnboardingTour.AddView.Body": "異なるレイアウトでボードを整理するための新しいビューを作成するには、ここに移動します。", + "OnboardingTour.AddView.Body": "異なるレイアウトでBoardを整理するための新しいビューを作成するには、ここに移動します。", "OnboardingTour.AddView.Title": "新しいビューを追加する", "OnboardingTour.CopyLink.Body": "リンクをコピーしてチャンネル、ダイレクトメッセージ、グループメッセージに貼り付けることで、カードをチームメイトと共有することができます。", "OnboardingTour.CopyLink.Title": "リンクをコピー", - "OnboardingTour.OpenACard.Body": "カードを開き、あなたの仕事を整理するのに役立つボードの便利な使い方を探ってみてください。", + "OnboardingTour.OpenACard.Body": "カードを開き、あなたの仕事を整理するのに役立つBoardの便利な使い方を探ってみてください。", "OnboardingTour.OpenACard.Title": "カードを開く", - "OnboardingTour.ShareBoard.Body": "作成したボードは、社内やチーム内で共有することも、組織外から見えるように公開することも可能です。", - "OnboardingTour.ShareBoard.Title": "ボードを共有する", + "OnboardingTour.ShareBoard.Body": "作成したBoardは、社内やチーム内で共有することも、組織外から見えるように公開することも可能です。", + "OnboardingTour.ShareBoard.Title": "Boardを共有", "PersonProperty.board-members": "Board members", "PersonProperty.me": "私", "PersonProperty.non-board-members": "Not board members", @@ -231,7 +231,7 @@ "ShareBoard.PublishTitle": "Web上へ公開する", "ShareBoard.ShareInternal": "内部で共有する", "ShareBoard.ShareInternalDescription": "権限のあるユーザーは、このリンクを使用することができます。", - "ShareBoard.Title": "ボードを共有する", + "ShareBoard.Title": "Boardを共有", "ShareBoard.confirmRegenerateToken": "実行すると以前に共有されたリンクは無効になります。続行しますか?", "ShareBoard.copiedLink": "コピーしました!", "ShareBoard.copyLink": "リンクをコピー", @@ -244,40 +244,40 @@ "ShareTemplate.Title": "テンプレートを共有する", "ShareTemplate.searchPlaceholder": "人を検索", "Sidebar.about": "Focalboardについて", - "Sidebar.add-board": "+ ボードを追加する", + "Sidebar.add-board": "+ Boardを追加", "Sidebar.changePassword": "パスワードを変更する", - "Sidebar.delete-board": "ボードを削除", - "Sidebar.duplicate-board": "ボードを複製する", + "Sidebar.delete-board": "Boardを削除", + "Sidebar.duplicate-board": "Boardを複製する", "Sidebar.export-archive": "エクスポート", "Sidebar.import": "インポート", "Sidebar.import-archive": "インポート", "Sidebar.invite-users": "ユーザーを招待する", "Sidebar.logout": "ログアウト", "Sidebar.new-category.badge": "新規", - "Sidebar.new-category.drag-boards-cta": "ここにボードをドラッグ...", - "Sidebar.no-boards-in-category": "カテゴリ内にボードがありません", + "Sidebar.new-category.drag-boards-cta": "ここにBoardをドラッグ...", + "Sidebar.no-boards-in-category": "カテゴリ内にBoardがありません", "Sidebar.product-tour": "プロダクトツアー", "Sidebar.random-icons": "ランダムアイコン", "Sidebar.set-language": "言語設定", "Sidebar.set-theme": "テーマ設定", "Sidebar.settings": "設定", - "Sidebar.template-from-board": "ボードからの新しいテンプレート", - "Sidebar.untitled-board": "(無題のボード)", + "Sidebar.template-from-board": "Boardからの新しいテンプレート", + "Sidebar.untitled-board": "(無題のBoard)", "Sidebar.untitled-view": "(無題のビュー)", "SidebarCategories.BlocksMenu.Move": "移動...", "SidebarCategories.CategoryMenu.CreateNew": "新しいカテゴリを作成する", "SidebarCategories.CategoryMenu.Delete": "カテゴリを削除する", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "{categoryName} にあるボードは、Boards カテゴリに戻されます。どのボードからも削除されることはありません。", + "SidebarCategories.CategoryMenu.DeleteModal.Body": "{categoryName} にあるBoardは、Boards カテゴリに戻されます。どのBoardからも削除されることはありません。", "SidebarCategories.CategoryMenu.DeleteModal.Title": "このカテゴリを削除しますか?", "SidebarCategories.CategoryMenu.Update": "カテゴリ名を変更する", - "SidebarTour.ManageCategories.Body": "カスタムカテゴリーを作成し、管理することができます。カテゴリはユーザーごとに設定されるため、ボードを自分のカテゴリに移動しても、同じボードを使用している他のメンバーには影響がありません。", + "SidebarTour.ManageCategories.Body": "カスタムカテゴリーを作成し、管理することができます。カテゴリはユーザーごとに設定されるため、Boardを自分のカテゴリに移動しても、同じBoardを使用している他のメンバーには影響がありません。", "SidebarTour.ManageCategories.Title": "カテゴリー管理", - "SidebarTour.SearchForBoards.Body": "ボード切り替え(Cmd/Ctrl + K)により、素早くボードを検索し、サイドバーに追加することができます。", - "SidebarTour.SearchForBoards.Title": "ボードを検索", - "SidebarTour.SidebarCategories.Body": "すべてのボードが新しいサイドバーの下に整理されました。もう、ワークスペースを切り替える必要はありません。v7.2へのアップグレードに伴い、以前のワークスペースに基づいたカスタムカテゴリーが自動的に作成されている場合があります。これらは、お好みで削除したり編集することができます。", + "SidebarTour.SearchForBoards.Body": "Board切替(Cmd/Ctrl + K)により、素早くBoardを検索し、サイドバーに追加することができます。", + "SidebarTour.SearchForBoards.Title": "Boardを検索", + "SidebarTour.SidebarCategories.Body": "すべてのBoardが新しいサイドバーの下に整理されました。もう、ワークスペースを切り替える必要はありません。v7.2へのアップグレードに伴い、以前のワークスペースに基づいたカスタムカテゴリーが自動的に作成されている場合があります。これらは、お好みで削除したり編集することができます。", "SidebarTour.SidebarCategories.Link": "詳細", "SidebarTour.SidebarCategories.Title": "サイドバーカテゴリー", - "SiteStats.total_boards": "ボード数", + "SiteStats.total_boards": "Board総数", "SiteStats.total_cards": "カード数", "TableComponent.add-icon": "アイコンを追加する", "TableComponent.name": "名前", @@ -307,11 +307,11 @@ "ValueSelectorLabel.openMenu": "メニューを開く", "VersionMessage.help": "このバージョンの新機能を確認する。", "View.AddView": "ビューを追加", - "View.Board": "ボード", + "View.Board": "Board", "View.DeleteView": "ビューを削除", "View.DuplicateView": "ビューを複製", "View.Gallery": "ギャラリー", - "View.NewBoardTitle": "ボード表示", + "View.NewBoardTitle": "Board表示", "View.NewCalendarTitle": "カレンダー表示", "View.NewGalleryTitle": "ギャラリービュー", "View.NewTableTitle": "テーブル表示", @@ -323,7 +323,7 @@ "ViewHeader.display-by": "表示対象: {property}", "ViewHeader.edit-template": "編集", "ViewHeader.empty-card": "空のカード", - "ViewHeader.export-board-archive": "ボードアーカイブのエクスポート", + "ViewHeader.export-board-archive": "Boardアーカイブのエクスポート", "ViewHeader.export-complete": "エクスポートが完了しました!", "ViewHeader.export-csv": "CSVエクスポート", "ViewHeader.export-failed": "エクスポートが失敗しました!", @@ -339,7 +339,7 @@ "ViewHeader.untitled": "無題", "ViewHeader.view-header-menu": "ヘッダーメニューを見る", "ViewHeader.view-menu": "メニューを見る", - "ViewLimitDialog.Heading": "ボードごとのビュー数制限に達しました", + "ViewLimitDialog.Heading": "Boardごとのビュー数制限に達しました", "ViewLimitDialog.PrimaryButton.Title.Admin": "アップグレード", "ViewLimitDialog.PrimaryButton.Title.RegularUser": "管理者に通知する", "ViewLimitDialog.Subtext.Admin": "ProfessionalプランまたはEnterpriseプランにアップグレードしてください。", @@ -352,22 +352,22 @@ "ViewTitle.random-icon": "ランダム", "ViewTitle.remove-icon": "アイコンを削除する", "ViewTitle.show-description": "説明を表示", - "ViewTitle.untitled-board": "無題のボード", + "ViewTitle.untitled-board": "無題のBoard", "WelcomePage.Description": "Boardsは、よく知られたKanban形式のビューを使用して、チーム全体の作業を定義、整理、追跡、管理するためのプロジェクト管理ツールです。", "WelcomePage.Explore.Button": "ツアーに参加する", - "WelcomePage.Heading": "ボードへようこそ", + "WelcomePage.Heading": "Boardへようこそ", "WelcomePage.NoThanks.Text": "いいえ、自分で調べます", "WelcomePage.StartUsingIt.Text": "利用を開始する", - "Workspace.editing-board-template": "ボードのテンプレートを編集しています。", + "Workspace.editing-board-template": "Boardのテンプレートを編集しています。", "badge.guest": "ゲスト", - "boardSelector.confirm-link-board": "ボードをチャンネルへリンク", - "boardSelector.confirm-link-board-button": "はい、ボードをリンクします", - "boardSelector.confirm-link-board-subtext": "\"{boardName}\" をチャンネルにリンクすると、チャンネルの(既存/新規)メンバー全員がボードを編集できるようになります。ただし、ゲストユーザーは除外されます。ボードとチャンネルのリンク解除はいつでも可能です。", - "boardSelector.confirm-link-board-subtext-with-other-channel": "\"{boardName}\" をチャンネルにリンクすると、チャンネルの(既存/新規)メンバー全員がボードを編集できるようになります。ただし、ゲストユーザーは除外されます。{lineBreak} このボードは現在他のチャンネルにリンクされています。ここにリンクさせると、他のチャンネルとのリンクは解除されます。", - "boardSelector.create-a-board": "ボードを作成", + "boardSelector.confirm-link-board": "Boardをチャンネルへリンク", + "boardSelector.confirm-link-board-button": "はい、Boardをリンクします", + "boardSelector.confirm-link-board-subtext": "\"{boardName}\" をチャンネルにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。Boardとチャンネルのリンク解除はいつでも可能です。", + "boardSelector.confirm-link-board-subtext-with-other-channel": "\"{boardName}\" をチャンネルにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。{lineBreak} このBoardは現在他のチャンネルにリンクされています。ここにリンクさせると、他のチャンネルとのリンクは解除されます。", + "boardSelector.create-a-board": "Boardを作成", "boardSelector.link": "リンク", - "boardSelector.search-for-boards": "ボードを検索", - "boardSelector.title": "ボードをリンク", + "boardSelector.search-for-boards": "Boardを検索", + "boardSelector.title": "Boardをリンク", "boardSelector.unlink": "リンク解除", "calendar.month": "月", "calendar.today": "今日", @@ -380,64 +380,68 @@ "default-properties.title": "タイトル", "error.back-to-home": "ホームへ戻る", "error.back-to-team": "チームに戻る", - "error.board-not-found": "ボードが見つかりませんでした。", + "error.board-not-found": "Boardが見つかりませんでした。", "error.go-login": "ログイン", - "error.invalid-read-only-board": "このボードにアクセスできません。アクセスするにはログインしてください。", - "error.not-logged-in": "セッションの有効期限が切れているか、ログインしていない可能性があります。ボードにアクセスするには再度ログインしてください。", + "error.invalid-read-only-board": "このBoardにアクセスできません。アクセスするにはBoardsにログインしてください。", + "error.not-logged-in": "セッションの有効期限が切れているか、ログインしていない可能性があります。Boardsにアクセスするには再度ログインしてください。", "error.page.title": "申し訳ありませんが、何か問題が発生しました", "error.team-undefined": "有効なチームではありません。", "error.unknown": "エラーが発生しました。", "generic.previous": "前へ", - "guest-no-board.subtitle": "あなたはまだこのチームのどのボードにもアクセスできません。誰かがあなたをボードに追加するまでお待ちください。", - "guest-no-board.title": "まだボードはありません", + "guest-no-board.subtitle": "あなたはまだこのチームのどのBoardにもアクセスできません。誰かがあなたをBoardに追加するまでお待ちください。", + "guest-no-board.title": "まだBoardsはありません", "imagePaste.upload-failed": "ファイルサイズの制限に達しているため、一部のファイルをアップロードできませんでした。", "limitedCard.title": "非表示カード", "login.log-in-button": "ログイン", "login.log-in-title": "ログイン", "login.register-button": "アカウントをお持ちでない方はアカウントを作成してください", + "new_channel_modal.create_board.empty_board_description": "空のBoardを新規作成する", + "new_channel_modal.create_board.empty_board_title": "空のBoard", + "new_channel_modal.create_board.select_template_placeholder": "テンプレートを選択", + "new_channel_modal.create_board.title": "このチャンネル用のBoardを作成する", "notification-box-card-limit-reached.close-tooltip": "10日間のスヌーズ", "notification-box-card-limit-reached.contact-link": "管理者に通知する", "notification-box-card-limit-reached.link": "有料プランへのアップグレード", - "notification-box-card-limit-reached.title": "ボードから {cards} カードが非表示になっています", + "notification-box-card-limit-reached.title": "Boardから {cards} カードが非表示になっています", "notification-box-cards-hidden.title": "このアクションにより他のカードが非表示になります", "notification-box.card-limit-reached.not-admin.text": "アーカイブされたカードにアクセスするには、{contactLink}から有料プランにアップグレードしてください。", "notification-box.card-limit-reached.text": "カード数の制限に達しました。古いカードを閲覧するには、{link}", - "person.add-user-to-board": "{username} をボードに追加", - "person.add-user-to-board-confirm-button": "ボードに追加", + "person.add-user-to-board": "{username} をBoardに追加", + "person.add-user-to-board-confirm-button": "Boardに追加", "person.add-user-to-board-permissions": "権限", - "person.add-user-to-board-question": "{username} をボードに追加しますか?", - "person.add-user-to-board-warning": "{username} はボードのメンバーではないので、それに関する通知を受け取ることはありません。", + "person.add-user-to-board-question": "{username} をBoardに追加しますか?", + "person.add-user-to-board-warning": "{username} はBoardのメンバーではないので、それに関する通知を受け取ることはありません。", "register.login-button": "または、すでにアカウントをお持ちの方はログインしてください", "register.signup-title": "アカウント登録", - "rhs-board-non-admin-msg": "あなたはボードの管理者ではありません", + "rhs-board-non-admin-msg": "あなたはBoardの管理者ではありません", "rhs-boards.add": "追加", "rhs-boards.dm": "DM", "rhs-boards.gm": "GM", "rhs-boards.header.dm": "このダイレクトメッセージ", "rhs-boards.header.gm": "このグループメッセージ", "rhs-boards.last-update-at": "最終更新: {datetime}", - "rhs-boards.link-boards-to-channel": "ボードを{channelName}へリンクする", - "rhs-boards.linked-boards": "リンク済みボード", - "rhs-boards.no-boards-linked-to-channel": "{channelName}にリンクされたボードはまだありません", + "rhs-boards.link-boards-to-channel": "Boardsを{channelName}へリンクする", + "rhs-boards.linked-boards": "リンク済みBoards", + "rhs-boards.no-boards-linked-to-channel": "{channelName}にリンクされたBoardsはまだありません", "rhs-boards.no-boards-linked-to-channel-description": "Boardsは、よく知られたKanban形式のビューを使用して、チーム全体の作業を定義、生理、追跡、管理するためのプロジェクト管理ツールです。", - "rhs-boards.unlink-board": "ボードのリンクを解除", - "rhs-boards.unlink-board1": "ボードをリンク解除", - "rhs-channel-boards-header.title": "ボード", + "rhs-boards.unlink-board": "Boardのリンクを解除", + "rhs-boards.unlink-board1": "Boardのリンクを解除", + "rhs-channel-boards-header.title": "Boards", "share-board.publish": "公開", "share-board.share": "共有", "shareBoard.channels-select-group": "Channels", - "shareBoard.confirm-change-team-role.body": "このボードで \"{role}\" より弱い権限のユーザー全員が {role} に昇格します。本当にボードの最低限のロールを変更しますか?", + "shareBoard.confirm-change-team-role.body": "このBoardで \"{role}\" より弱い権限のユーザー全員が {role} に昇格します。本当にBoardの最低限のロールを変更しますか?", "shareBoard.confirm-change-team-role.confirmBtnText": "最低限のロールを変更", "shareBoard.confirm-change-team-role.title": "最低限のロールを変更", - "shareBoard.confirm-link-channel": "ボードをチャンネルへリンク", + "shareBoard.confirm-link-channel": "Boardをチャンネルへリンク", "shareBoard.confirm-link-channel-button": "チャンネルにリンク", "shareBoard.confirm-link-channel-button-with-other-channel": "リンク解除とリンクはこちら", - "shareBoard.confirm-link-channel-subtext": "チャンネルをボードにリンクすると、チャンネルの(既存/新規)メンバー全員がボードを編集できるようになります。ただし、ゲストユーザーは除外されます。", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "チャンネルをボードにリンクすると、チャンネルの(既存/新規)メンバー全員がボードを編集できるようになります。ただし、ゲストユーザーは除外されます。{lineBreak} このボードは現在他のチャンネルにリンクされています。ここにリンクさせると、他のチャンネルとのリンクは解除されます。", - "shareBoard.confirm-unlink.body": "ボードからチャンネルのリンクを解除すると、別途権限を付与されない限り、チャンネルの(既存/新規)メンバー全員がボードへアクセスできなくなります。", + "shareBoard.confirm-link-channel-subtext": "チャンネルをBoardにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。", + "shareBoard.confirm-link-channel-subtext-with-other-channel": "チャンネルをBoardにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。{lineBreak} このBoardは現在他のチャンネルにリンクされています。ここにリンクさせると、他のチャンネルとのリンクは解除されます。", + "shareBoard.confirm-unlink.body": "Boardからチャンネルへのリンクを解除すると、別途権限を付与されない限り、チャンネルの(既存/新規)メンバー全員がBoardへアクセスできなくなります。", "shareBoard.confirm-unlink.confirmBtnText": "チャンネルとのリンクを解除", - "shareBoard.confirm-unlink.title": "ボードからチャンネルへのリンクを解除する", - "shareBoard.lastAdmin": "ボードには少なくとも1名の管理者が必要です", + "shareBoard.confirm-unlink.title": "Boardからチャンネルへのリンクを解除する", + "shareBoard.lastAdmin": "Boardsには少なくとも1名の管理者が必要です", "shareBoard.members-select-group": "メンバー", "shareBoard.unknown-channel-display-name": "不明なチャンネル", "tutorial_tip.finish_tour": "完了", From 1b917ba44df75241efb2bec9fcbf88cdc51dfb2e Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Mon, 13 Feb 2023 16:16:32 +0100 Subject: [PATCH 17/56] Translated using Weblate (Croatian) Currently translated at 100.0% (450 of 450 strings) Translation: Focalboard/webapp Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/hr/ --- webapp/i18n/hr.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webapp/i18n/hr.json b/webapp/i18n/hr.json index a8ae70b1f..2af1f2009 100644 --- a/webapp/i18n/hr.json +++ b/webapp/i18n/hr.json @@ -395,6 +395,10 @@ "login.log-in-button": "Prijavi se", "login.log-in-title": "Prijavi se", "login.register-button": "ili stvori račun, ako ga još nemaš", + "new_channel_modal.create_board.empty_board_description": "Stvori novu praznu ploču", + "new_channel_modal.create_board.empty_board_title": "Prazna ploča", + "new_channel_modal.create_board.select_template_placeholder": "Odaberi predložak", + "new_channel_modal.create_board.title": "Stvori ploču za ovaj kanal", "notification-box-card-limit-reached.close-tooltip": "Postavi pripravno stanje na 10 dana", "notification-box-card-limit-reached.contact-link": "obavijesti svog administratora", "notification-box-card-limit-reached.link": "Nadogradi na plaćenu tarifu", From cc9a689c8bd0b01194c423726a5ef0fc812cb8a1 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Tue, 14 Feb 2023 03:00:15 +0530 Subject: [PATCH 18/56] Added the bot-notification changes for attachment (#4301) * Added the bot-notification changes for attachment * Linter fixes * Added Todo for i18n * Grammer fix * Statement Corrections * Grammer fix --------- Co-authored-by: Mattermost Build --- .../diff2slackattachments.go | 28 +++++++++++++++++++ webapp/src/components/cardDialog.tsx | 1 + 2 files changed, 29 insertions(+) diff --git a/server/services/notify/notifysubscriptions/diff2slackattachments.go b/server/services/notify/notifysubscriptions/diff2slackattachments.go index 31c66768b..32ec9b6d0 100644 --- a/server/services/notify/notifysubscriptions/diff2slackattachments.go +++ b/server/services/notify/notifysubscriptions/diff2slackattachments.go @@ -189,6 +189,9 @@ func cardDiff2SlackAttachment(cardDiff *Diff, opts DiffConvOpts) (*mm_model.Slac // comment add/delete attachment.Fields = appendCommentChanges(attachment.Fields, cardDiff) + // File Attachment add/delete + attachment.Fields = appendAttachmentChanges(attachment.Fields, cardDiff) + // content/description changes attachment.Fields = appendContentChanges(attachment.Fields, cardDiff, opts.Logger) @@ -264,6 +267,31 @@ func appendCommentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Dif return fields } +func appendAttachmentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff) []*mm_model.SlackAttachmentField { + for _, child := range cardDiff.Diffs { + if child.BlockType == model.TypeAttachment { + var format string + var msg string + if child.NewBlock != nil && child.OldBlock == nil { + format = "Added an attachment: **`%s`**" + msg = child.NewBlock.Title + } else { + format = "Removed ~~`%s`~~ attachment" + msg = stripNewlines(child.OldBlock.Title) + } + + if format != "" { + fields = append(fields, &mm_model.SlackAttachmentField{ + Short: false, + Title: "Changed by " + makeAuthorsList(child.Authors, "unknown_user"), // TODO: localize this when server has i18n + Value: fmt.Sprintf(format, msg), + }) + } + } + } + return fields +} + func appendContentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff, logger mlog.LoggerIFace) []*mm_model.SlackAttachmentField { for _, child := range cardDiff.Diffs { var opAdd, opDelete bool diff --git a/webapp/src/components/cardDialog.tsx b/webapp/src/components/cardDialog.tsx index 6e15c5310..da13e035d 100644 --- a/webapp/src/components/cardDialog.tsx +++ b/webapp/src/components/cardDialog.tsx @@ -182,6 +182,7 @@ const CardDialog = (props: Props): JSX.Element => { removeUploadingAttachment(uploadingBlock) const block = createAttachmentBlock() block.fields.attachmentId = attachmentId || '' + block.title = attachment.name sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.uploadSuccess', defaultMessage: 'Attachment uploaded successfull.'}), severity: 'normal'}) resolve(block) } else { From 759a8bb76ad937a6f62fbb7e1b14166fef45143d Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Tue, 14 Feb 2023 08:32:37 -0700 Subject: [PATCH 19/56] Fix today (#4531) * set date from today click to 12pm UTC * make sure all dates are set to 12pm * fix test --------- Co-authored-by: Mattermost Build --- webapp/src/properties/date/date.test.tsx | 2 +- webapp/src/properties/date/date.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/src/properties/date/date.test.tsx b/webapp/src/properties/date/date.test.tsx index 056d06550..5a42f5ef7 100644 --- a/webapp/src/properties/date/date.test.tsx +++ b/webapp/src/properties/date/date.test.tsx @@ -302,7 +302,7 @@ describe('properties/dateRange', () => { // About `Date()` // > "When called as a function, returns a string representation of the current date and time" const date = new Date() - const today = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) + const today = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12) const {getByText, getByTitle} = render(component) const dayDisplay = getByText('Empty') diff --git a/webapp/src/properties/date/date.tsx b/webapp/src/properties/date/date.tsx index 22de53fa8..23d764a1b 100644 --- a/webapp/src/properties/date/date.tsx +++ b/webapp/src/properties/date/date.tsx @@ -97,6 +97,7 @@ function DateRange(props: PropertyProps): JSX.Element { const handleDayClick = (day: Date) => { const range: DateProperty = {} + day.setHours(12) if (isRange) { const newRange = DateUtils.addDayToRange(day, {from: dateFrom, to: dateTo}) range.from = newRange.from?.getTime() From 098868387e20065085d9cf373708088a9eb5d360 Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Tue, 14 Feb 2023 09:17:33 -0700 Subject: [PATCH 20/56] initial implementation of SysAdmin/TeamAdmin feature (#4537) * initial implementation of SysAdmin/TeamAdmin feature * fix adminBadge tests * updating tests * more fixes for unit tests * lint fixes * update snapshots * update cypress test for call change * add additional unit tests * update test for lint errors * fix reviews implement tests * fix for merge, reset dialog before redirection * remove unused test code * fix more tests * fix swagger doc for missing parameters --------- Co-authored-by: Mattermost Build --- server/api/members.go | 19 +- server/api/teams.go | 105 +++++++ server/api/users.go | 15 + server/app/boards.go | 49 +++- server/app/boards_test.go | 116 +++++++- server/app/category_boards_test.go | 4 + server/app/helper_test.go | 10 + server/app/onboarding_test.go | 2 +- server/app/user.go | 15 +- server/app/user_test.go | 64 +++++ server/integrationtests/clienttestlib.go | 3 + server/model/permission.go | 2 + server/model/user.go | 3 + .../localpermissions/localpermissions.go | 3 + .../localpermissions/localpermissions_test.go | 28 ++ .../permissions/mmpermissions/helpers_test.go | 14 + .../mmpermissions/mmpermissions.go | 8 +- .../mmpermissions/mmpermissions_test.go | 21 ++ .../__snapshots__/shareBoard.test.tsx.snap | 18 ++ .../userPermissionsRow.test.tsx.snap | 260 ++++++++++++++++++ .../components/shareBoard/shareBoard.test.tsx | 4 +- .../src/components/shareBoard/shareBoard.tsx | 2 + .../shareBoard/userPermissionsRow.test.tsx | 32 +++ .../shareBoard/userPermissionsRow.tsx | 2 + webapp/src/octoClient.ts | 32 ++- webapp/src/pages/boardPage/boardPage.tsx | 154 +++++++---- webapp/src/store/boards.ts | 12 +- webapp/src/user.tsx | 1 + .../__snapshots__/adminBadge.test.tsx.snap | 29 ++ webapp/src/widgets/adminBadge/adminBadge.scss | 16 ++ .../widgets/adminBadge/adminBadge.test.tsx | 32 +++ webapp/src/widgets/adminBadge/adminBadge.tsx | 36 +++ 32 files changed, 1031 insertions(+), 80 deletions(-) create mode 100644 server/app/user_test.go create mode 100644 webapp/src/widgets/adminBadge/__snapshots__/adminBadge.test.tsx.snap create mode 100644 webapp/src/widgets/adminBadge/adminBadge.scss create mode 100644 webapp/src/widgets/adminBadge/adminBadge.test.tsx create mode 100644 webapp/src/widgets/adminBadge/adminBadge.tsx diff --git a/server/api/members.go b/server/api/members.go index 9937fd9dc..f60d00db9 100644 --- a/server/api/members.go +++ b/server/api/members.go @@ -206,6 +206,11 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) { // description: Board ID // required: true // type: string + // - name: allow_admin + // in: path + // description: allows admin users to join private boards + // required: false + // type: boolean // security: // - BearerAuth: [] // responses: @@ -222,6 +227,9 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) { // schema: // "$ref": "#/definitions/ErrorResponse" + query := r.URL.Query() + allowAdmin := query.Has("allow_admin") + userID := getUserID(r) if userID == "" { a.errorResponse(w, r, model.NewErrBadRequest("missing user ID")) @@ -234,9 +242,14 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) { a.errorResponse(w, r, err) return } + + isAdmin := false if board.Type != model.BoardTypeOpen { - a.errorResponse(w, r, model.NewErrPermission("cannot join a non Open board")) - return + if !allowAdmin || !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) { + a.errorResponse(w, r, model.NewErrPermission("cannot join a non Open board")) + return + } + isAdmin = true } if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { @@ -257,7 +270,7 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) { newBoardMember := &model.BoardMember{ UserID: userID, BoardID: boardID, - SchemeAdmin: board.MinimumRole == model.BoardRoleAdmin, + SchemeAdmin: board.MinimumRole == model.BoardRoleAdmin || isAdmin, SchemeEditor: board.MinimumRole == model.BoardRoleNone || board.MinimumRole == model.BoardRoleEditor, SchemeCommenter: board.MinimumRole == model.BoardRoleCommenter, SchemeViewer: board.MinimumRole == model.BoardRoleViewer, diff --git a/server/api/teams.go b/server/api/teams.go index 2d18c86f8..50c36ac1a 100644 --- a/server/api/teams.go +++ b/server/api/teams.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "io" "net/http" "github.com/gorilla/mux" @@ -15,6 +16,7 @@ func (a *API) registerTeamsRoutes(r *mux.Router) { r.HandleFunc("/teams", a.sessionRequired(a.handleGetTeams)).Methods("GET") r.HandleFunc("/teams/{teamID}", a.sessionRequired(a.handleGetTeam)).Methods("GET") r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsers)).Methods("GET") + r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsersByID)).Methods("POST") r.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET") } @@ -257,3 +259,106 @@ func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) { auditRec.AddMeta("userCount", len(users)) auditRec.Success() } + +func (a *API) handleGetTeamUsersByID(w http.ResponseWriter, r *http.Request) { + // swagger:operation POST /teams/{teamID}/users getTeamUsersByID + // + // Returns a user[] + // + // --- + // produces: + // - application/json + // parameters: + // - name: teamID + // in: path + // description: Team ID + // required: true + // type: string + // - name: Body + // in: body + // description: []UserIDs to return + // required: true + // type: []string + // security: + // - BearerAuth: [] + // responses: + // '200': + // description: success + // schema: + // type: array + // items: + // "$ref": "#/definitions/User" + // default: + // description: internal error + // schema: + // "$ref": "#/definitions/ErrorResponse" + + requestBody, err := io.ReadAll(r.Body) + if err != nil { + a.errorResponse(w, r, err) + return + } + + var userIDs []string + if err = json.Unmarshal(requestBody, &userIDs); err != nil { + a.errorResponse(w, r, err) + return + } + + auditRec := a.makeAuditRecord(r, "getTeamUsersByID", audit.Fail) + defer a.audit.LogRecord(audit.LevelRead, auditRec) + + vars := mux.Vars(r) + teamID := vars["teamID"] + userID := getUserID(r) + + if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { + a.errorResponse(w, r, model.NewErrPermission("access denied to team")) + return + } + + var users []*model.User + var error error + + if len(userIDs) == 0 { + a.errorResponse(w, r, model.NewErrBadRequest("User IDs are empty")) + return + } + + if userIDs[0] == model.SingleUser { + ws, _ := a.app.GetRootTeam() + now := utils.GetMillis() + user := &model.User{ + ID: model.SingleUser, + Username: model.SingleUser, + Email: model.SingleUser, + CreateAt: ws.UpdateAt, + UpdateAt: now, + } + users = append(users, user) + } else { + users, error = a.app.GetUsersList(userIDs) + if error != nil { + a.errorResponse(w, r, error) + return + } + + for i, u := range users { + if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) { + users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id) + } + if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) { + users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id) + } + } + } + + usersList, err := json.Marshal(users) + if err != nil { + a.errorResponse(w, r, err) + return + } + + jsonStringResponse(w, http.StatusOK, string(usersList)) + auditRec.Success() +} diff --git a/server/api/users.go b/server/api/users.go index 90932d4d6..08d447187 100644 --- a/server/api/users.go +++ b/server/api/users.go @@ -107,6 +107,12 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { // --- // produces: // - application/json + // parameters: + // - name: teamID + // in: path + // description: Team ID + // required: false + // type: string // security: // - BearerAuth: [] // responses: @@ -118,6 +124,8 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { // description: internal error // schema: // "$ref": "#/definitions/ErrorResponse" + query := r.URL.Query() + teamID := query.Get("teamID") userID := getUserID(r) @@ -146,6 +154,13 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { } } + if teamID != "" && a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionManageTeam) { + user.Permissions = append(user.Permissions, model.PermissionManageTeam.Id) + } + if a.permissions.HasPermissionTo(userID, model.PermissionManageSystem) { + user.Permissions = append(user.Permissions, model.PermissionManageSystem.Id) + } + userData, err := json.Marshal(user) if err != nil { a.errorResponse(w, r, err) diff --git a/server/app/boards.go b/server/app/boards.go index bb5d3ff34..2ddee918f 100644 --- a/server/app/boards.go +++ b/server/app/boards.go @@ -502,11 +502,48 @@ func (a *App) DeleteBoard(boardID, userID string) error { } func (a *App) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) { - return a.store.GetMembersForBoard(boardID) + members, err := a.store.GetMembersForBoard(boardID) + if err != nil { + return nil, err + } + + board, err := a.store.GetBoard(boardID) + if err != nil && !model.IsErrNotFound(err) { + return nil, err + } + if board != nil { + for i, m := range members { + if !m.SchemeAdmin { + if a.permissions.HasPermissionToTeam(m.UserID, board.TeamID, model.PermissionManageTeam) { + members[i].SchemeAdmin = true + } + } + } + } + return members, nil } func (a *App) GetMembersForUser(userID string) ([]*model.BoardMember, error) { - return a.store.GetMembersForUser(userID) + members, err := a.store.GetMembersForUser(userID) + if err != nil { + return nil, err + } + + for i, m := range members { + if !m.SchemeAdmin { + board, err := a.store.GetBoard(m.BoardID) + if err != nil && !model.IsErrNotFound(err) { + return nil, err + } + if board != nil { + if a.permissions.HasPermissionToTeam(m.UserID, board.TeamID, model.PermissionManageTeam) { + // if system/team admin + members[i].SchemeAdmin = true + } + } + } + } + return members, nil } func (a *App) GetMemberForBoard(boardID string, userID string) (*model.BoardMember, error) { @@ -536,6 +573,14 @@ func (a *App) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, e return nil, err } + if !newMember.SchemeAdmin { + if board != nil { + if a.permissions.HasPermissionToTeam(newMember.UserID, board.TeamID, model.PermissionManageTeam) { + newMember.SchemeAdmin = true + } + } + } + if !board.IsTemplate { if err = a.addBoardsToDefaultCategory(member.UserID, board.TeamID, []*model.Board{board}); err != nil { return nil, err diff --git a/server/app/boards_test.go b/server/app/boards_test.go index 51bb88296..fc9771e54 100644 --- a/server/app/boards_test.go +++ b/server/app/boards_test.go @@ -127,6 +127,7 @@ func TestAddMemberToBoard(t *testing.T) { }, }, nil).Times(2) th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "default_category_id", []string{"board_id_1"}).Return(nil) + th.API.EXPECT().HasPermissionToTeam("user_id_1", "team_id_1", model.PermissionManageTeam).Return(false).Times(1) addedBoardMember, err := th.App.AddMemberToBoard(boardMember) require.NoError(t, err) @@ -180,7 +181,7 @@ func TestPatchBoard(t *testing.T) { ID: boardID, TeamID: teamID, IsTemplate: true, - }, nil) + }, nil).Times(2) // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil) @@ -218,7 +219,7 @@ func TestPatchBoard(t *testing.T) { ID: boardID, TeamID: teamID, IsTemplate: true, - }, nil) + }, nil).Times(2) // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil) @@ -256,7 +257,7 @@ func TestPatchBoard(t *testing.T) { ID: boardID, TeamID: teamID, IsTemplate: true, - }, nil) + }, nil).Times(2) // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) @@ -294,7 +295,7 @@ func TestPatchBoard(t *testing.T) { ID: boardID, TeamID: teamID, IsTemplate: true, - }, nil) + }, nil).Times(2) // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) @@ -332,7 +333,10 @@ func TestPatchBoard(t *testing.T) { ID: boardID, TeamID: teamID, IsTemplate: true, - }, nil) + }, nil).Times(3) + + th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) + // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) @@ -370,7 +374,11 @@ func TestPatchBoard(t *testing.T) { ID: boardID, TeamID: teamID, IsTemplate: true, - }, nil) + ChannelID: "", + }, nil).Times(1) + + th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) + // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) @@ -566,3 +574,99 @@ func TestDuplicateBoard(t *testing.T) { assert.NotNil(t, members) }) } + +func TestGetMembersForBoard(t *testing.T) { + th, tearDown := SetupTestHelper(t) + defer tearDown() + + const boardID = "board_id_1" + const userID = "user_id_1" + const teamID = "team_id_1" + + th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{ + { + BoardID: boardID, + UserID: userID, + SchemeEditor: true, + }, + }, nil).Times(3) + th.Store.EXPECT().GetBoard(boardID).Return(nil, nil).Times(1) + t.Run("-base case", func(t *testing.T) { + members, err := th.App.GetMembersForBoard(boardID) + assert.NoError(t, err) + assert.NotNil(t, members) + assert.False(t, members[0].SchemeAdmin) + }) + + board := &model.Board{ + ID: boardID, + TeamID: teamID, + } + th.Store.EXPECT().GetBoard(boardID).Return(board, nil).Times(2) + th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) + + t.Run("-team check false ", func(t *testing.T) { + members, err := th.App.GetMembersForBoard(boardID) + assert.NoError(t, err) + assert.NotNil(t, members) + + assert.False(t, members[0].SchemeAdmin) + }) + + th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) + t.Run("-team check true", func(t *testing.T) { + members, err := th.App.GetMembersForBoard(boardID) + assert.NoError(t, err) + assert.NotNil(t, members) + + assert.True(t, members[0].SchemeAdmin) + }) +} + +func TestGetMembersForUser(t *testing.T) { + th, tearDown := SetupTestHelper(t) + defer tearDown() + + const boardID = "board_id_1" + const userID = "user_id_1" + const teamID = "team_id_1" + + th.Store.EXPECT().GetMembersForUser(userID).Return([]*model.BoardMember{ + { + BoardID: boardID, + UserID: userID, + SchemeEditor: true, + }, + }, nil).Times(3) + th.Store.EXPECT().GetBoard(boardID).Return(nil, nil) + t.Run("-base case", func(t *testing.T) { + members, err := th.App.GetMembersForUser(userID) + assert.NoError(t, err) + assert.NotNil(t, members) + assert.False(t, members[0].SchemeAdmin) + }) + + board := &model.Board{ + ID: boardID, + TeamID: teamID, + } + th.Store.EXPECT().GetBoard(boardID).Return(board, nil).Times(2) + + th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) + t.Run("-team check false ", func(t *testing.T) { + members, err := th.App.GetMembersForUser(userID) + assert.NoError(t, err) + assert.NotNil(t, members) + + assert.False(t, members[0].SchemeAdmin) + }) + + th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) + t.Run("-team check true", func(t *testing.T) { + members, err := th.App.GetMembersForUser(userID) + assert.NoError(t, err) + assert.NotNil(t, members) + + assert.True(t, members[0].SchemeAdmin) + }) +} diff --git a/server/app/category_boards_test.go b/server/app/category_boards_test.go index 1ff0c2bf1..02a7d930e 100644 --- a/server/app/category_boards_test.go +++ b/server/app/category_boards_test.go @@ -58,6 +58,7 @@ func TestGetUserCategoryBoards(t *testing.T) { Synthetic: false, }, }, nil) + th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3) th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1", "board_id_2", "board_id_3"}).Return(nil) categoryBoards, err := th.App.GetUserCategoryBoards("user_id", "team_id") @@ -151,6 +152,7 @@ func TestCreateBoardsCategory(t *testing.T) { Synthetic: true, }, }, nil) + th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3) existingCategoryBoards := []model.CategoryBoards{} boardsCategory, err := th.App.createBoardsCategory("user_id", "team_id", existingCategoryBoards) @@ -195,6 +197,7 @@ func TestCreateBoardsCategory(t *testing.T) { Synthetic: false, }, }, nil) + th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3) th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1", "board_id_2", "board_id_3"}).Return(nil) th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ @@ -244,6 +247,7 @@ func TestCreateBoardsCategory(t *testing.T) { Synthetic: true, }, }, nil) + th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3) th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1"}).Return(nil) th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ diff --git a/server/app/helper_test.go b/server/app/helper_test.go index b471ace1e..df80aba57 100644 --- a/server/app/helper_test.go +++ b/server/app/helper_test.go @@ -10,6 +10,9 @@ import ( "github.com/mattermost/focalboard/server/auth" "github.com/mattermost/focalboard/server/services/config" "github.com/mattermost/focalboard/server/services/metrics" + "github.com/mattermost/focalboard/server/services/permissions/mmpermissions" + mmpermissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mmpermissions/mocks" + permissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mocks" "github.com/mattermost/focalboard/server/services/store/mockstore" "github.com/mattermost/focalboard/server/services/webhook" "github.com/mattermost/focalboard/server/ws" @@ -23,6 +26,7 @@ type TestHelper struct { Store *mockstore.MockStore FilesBackend *mocks.FileBackend logger mlog.LoggerIFace + API *mmpermissionsMocks.MockAPI } func SetupTestHelper(t *testing.T) (*TestHelper, func()) { @@ -37,6 +41,10 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) { webhook := webhook.NewClient(&cfg, logger) metricsService := metrics.NewMetrics(metrics.InstanceInfo{}) + mockStore := permissionsMocks.NewMockStore(ctrl) + mockAPI := mmpermissionsMocks.NewMockAPI(ctrl) + permissions := mmpermissions.New(mockStore, mockAPI, mlog.CreateConsoleTestLogger(true, mlog.LvlError)) + appServices := Services{ Auth: auth, Store: store, @@ -45,6 +53,7 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) { Metrics: metricsService, Logger: logger, SkipTemplateInit: true, + Permissions: permissions, } app2 := New(&cfg, wsserver, appServices) @@ -60,5 +69,6 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) { Store: store, FilesBackend: filesBackend, logger: logger, + API: mockAPI, }, tearDown } diff --git a/server/app/onboarding_test.go b/server/app/onboarding_test.go index 90a77170d..a3f41715e 100644 --- a/server/app/onboarding_test.go +++ b/server/app/onboarding_test.go @@ -39,7 +39,7 @@ func TestPrepareOnboardingTour(t *testing.T) { nil, nil) th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil).Times(2) th.Store.EXPECT().GetMembersForBoard("board_id_2").Return([]*model.BoardMember{}, nil).Times(1) - th.Store.EXPECT().GetBoard(welcomeBoard.ID).Return(&welcomeBoard, nil).Times(1) + th.Store.EXPECT().GetBoard(welcomeBoard.ID).Return(&welcomeBoard, nil).Times(2) th.Store.EXPECT().GetBoard("board_id_2").Return(&welcomeBoard, nil).Times(1) th.Store.EXPECT().GetUsersByTeam("0", "", false, false).Return([]*model.User{}, nil) diff --git a/server/app/user.go b/server/app/user.go index af409187e..56d628510 100644 --- a/server/app/user.go +++ b/server/app/user.go @@ -10,7 +10,20 @@ func (a *App) GetTeamUsers(teamID string, asGuestID string) ([]*model.User, erro } func (a *App) SearchTeamUsers(teamID string, searchQuery string, asGuestID string, excludeBots bool) ([]*model.User, error) { - return a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID, excludeBots, a.config.ShowEmailAddress, a.config.ShowFullName) + users, err := a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID, excludeBots, a.config.ShowEmailAddress, a.config.ShowFullName) + if err != nil { + return nil, err + } + + for i, u := range users { + if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) { + users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id) + } + if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) { + users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id) + } + } + return users, nil } func (a *App) UpdateUserConfig(userID string, patch model.UserPreferencesPatch) ([]mmModel.Preference, error) { diff --git a/server/app/user_test.go b/server/app/user_test.go new file mode 100644 index 000000000..668282247 --- /dev/null +++ b/server/app/user_test.go @@ -0,0 +1,64 @@ +package app + +import ( + "testing" + + "github.com/mattermost/focalboard/server/model" + "github.com/stretchr/testify/assert" +) + +func TestSearchUsers(t *testing.T) { + th, tearDown := SetupTestHelper(t) + defer tearDown() + th.App.config.ShowEmailAddress = false + th.App.config.ShowFullName = false + + teamID := "team-id-1" + userID := "user-id-1" + + t.Run("return empty users", func(t *testing.T) { + th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{}, nil) + + users, err := th.App.SearchTeamUsers(teamID, "", "", true) + assert.NoError(t, err) + assert.Equal(t, 0, len(users)) + }) + + t.Run("return user", func(t *testing.T) { + th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil) + th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) + th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(false).Times(1) + + users, err := th.App.SearchTeamUsers(teamID, "", "", true) + assert.NoError(t, err) + assert.Equal(t, 1, len(users)) + assert.Equal(t, 0, len(users[0].Permissions)) + }) + + t.Run("return team admin", func(t *testing.T) { + th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil) + th.App.config.ShowEmailAddress = false + th.App.config.ShowFullName = false + th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) + th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(false).Times(1) + + users, err := th.App.SearchTeamUsers(teamID, "", "", true) + assert.NoError(t, err) + assert.Equal(t, 1, len(users)) + assert.Equal(t, users[0].Permissions[0], model.PermissionManageTeam.Id) + }) + + t.Run("return system admin", func(t *testing.T) { + th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil) + th.App.config.ShowEmailAddress = false + th.App.config.ShowFullName = false + th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) + th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(true).Times(1) + + users, err := th.App.SearchTeamUsers(teamID, "", "", true) + assert.NoError(t, err) + assert.Equal(t, 1, len(users)) + assert.Equal(t, users[0].Permissions[0], model.PermissionManageTeam.Id) + assert.Equal(t, users[0].Permissions[1], model.PermissionManageSystem.Id) + }) +} diff --git a/server/integrationtests/clienttestlib.go b/server/integrationtests/clienttestlib.go index 5cbe18bb9..87884d24f 100644 --- a/server/integrationtests/clienttestlib.go +++ b/server/integrationtests/clienttestlib.go @@ -78,6 +78,9 @@ func (*FakePermissionPluginAPI) HasPermissionTo(userID string, permission *mmMod } func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool { + if permission.Id == model.PermissionManageTeam.Id { + return false + } if userID == userNoTeamMember { return false } diff --git a/server/model/permission.go b/server/model/permission.go index 7d3d773f1..f503a0f3d 100644 --- a/server/model/permission.go +++ b/server/model/permission.go @@ -6,6 +6,8 @@ import ( var ( PermissionViewTeam = mmModel.PermissionViewTeam + PermissionManageTeam = mmModel.PermissionManageTeam + PermissionManageSystem = mmModel.PermissionManageSystem PermissionReadChannel = mmModel.PermissionReadChannel PermissionViewMembers = mmModel.PermissionViewMembers PermissionCreatePublicChannel = mmModel.PermissionCreatePublicChannel diff --git a/server/model/user.go b/server/model/user.go index 7b48cb0a4..37c1f946d 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -66,6 +66,9 @@ type User struct { // required: true IsGuest bool `json:"is_guest"` + // Special Permissions the user may have + Permissions []string `json:"permissions,omitempty"` + Roles string `json:"roles"` } diff --git a/server/services/permissions/localpermissions/localpermissions.go b/server/services/permissions/localpermissions/localpermissions.go index c45412870..e71894ff0 100644 --- a/server/services/permissions/localpermissions/localpermissions.go +++ b/server/services/permissions/localpermissions/localpermissions.go @@ -31,6 +31,9 @@ func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel if userID == "" || teamID == "" || permission == nil { return false } + if permission.Id == model.PermissionManageTeam.Id { + return false + } return true } diff --git a/server/services/permissions/localpermissions/localpermissions_test.go b/server/services/permissions/localpermissions/localpermissions_test.go index 81a9e5677..dc7db8148 100644 --- a/server/services/permissions/localpermissions/localpermissions_test.go +++ b/server/services/permissions/localpermissions/localpermissions_test.go @@ -27,6 +27,11 @@ func TestHasPermissionToTeam(t *testing.T) { hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageBoardCards) assert.True(t, hasPermission) }) + + t.Run("no users have PermissionManageTeam on teams", func(t *testing.T) { + hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageTeam) + assert.False(t, hasPermission) + }) } func TestHasPermissionToBoard(t *testing.T) { @@ -141,4 +146,27 @@ func TestHasPermissionToBoard(t *testing.T) { th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo) }) + + t.Run("Manage Team Permission ", func(t *testing.T) { + member := &model.BoardMember{ + UserID: "user-id", + BoardID: "board-id", + SchemeViewer: true, + } + + hasPermissionTo := []*mmModel.Permission{ + model.PermissionViewBoard, + } + + hasNotPermissionTo := []*mmModel.Permission{ + model.PermissionManageBoardType, + model.PermissionDeleteBoard, + model.PermissionManageBoardRoles, + model.PermissionShareBoard, + model.PermissionManageBoardCards, + model.PermissionManageBoardProperties, + } + + th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo) + }) } diff --git a/server/services/permissions/mmpermissions/helpers_test.go b/server/services/permissions/mmpermissions/helpers_test.go index fc4117e7d..ec022bc01 100644 --- a/server/services/permissions/mmpermissions/helpers_test.go +++ b/server/services/permissions/mmpermissions/helpers_test.go @@ -58,6 +58,13 @@ func (th *TestHelper) checkBoardPermissions(roleName string, member *model.Board Return(member, nil). Times(1) + if !member.SchemeAdmin { + th.api.EXPECT(). + HasPermissionToTeam(member.UserID, teamID, model.PermissionManageTeam). + Return(roleName == "elevated-admin"). + Times(1) + } + hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) assert.True(t, hasPermission) }) @@ -80,6 +87,13 @@ func (th *TestHelper) checkBoardPermissions(roleName string, member *model.Board Return(member, nil). Times(1) + if !member.SchemeAdmin { + th.api.EXPECT(). + HasPermissionToTeam(member.UserID, teamID, model.PermissionManageTeam). + Return(roleName == "elevated-admin"). + Times(1) + } + hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) assert.False(t, hasPermission) }) diff --git a/server/services/permissions/mmpermissions/mmpermissions.go b/server/services/permissions/mmpermissions/mmpermissions.go index 8239a31b8..71aaec1b6 100644 --- a/server/services/permissions/mmpermissions/mmpermissions.go +++ b/server/services/permissions/mmpermissions/mmpermissions.go @@ -82,7 +82,6 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod if !s.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { return false } - member, err := s.store.GetMemberForBoard(boardID, userID) if model.IsErrNotFound(err) { return false @@ -107,6 +106,13 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod member.SchemeViewer = true } + // Admins become member of boards, but get minimal role + // if they are a System/Team Admin (model.PermissionManageTeam) + // elevate their permissions + if !member.SchemeAdmin && s.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) { + return true + } + switch permission { case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments: return member.SchemeAdmin diff --git a/server/services/permissions/mmpermissions/mmpermissions_test.go b/server/services/permissions/mmpermissions/mmpermissions_test.go index 01a31b95f..ff14baec7 100644 --- a/server/services/permissions/mmpermissions/mmpermissions_test.go +++ b/server/services/permissions/mmpermissions/mmpermissions_test.go @@ -219,4 +219,25 @@ func TestHasPermissionToBoard(t *testing.T) { th.checkBoardPermissions("viewer", member, teamID, hasPermissionTo, hasNotPermissionTo) }) + + t.Run("elevate board viewer permissions", func(t *testing.T) { + member := &model.BoardMember{ + UserID: userID, + BoardID: boardID, + SchemeViewer: true, + } + + hasPermissionTo := []*mmModel.Permission{ + model.PermissionManageBoardType, + model.PermissionDeleteBoard, + model.PermissionManageBoardRoles, + model.PermissionShareBoard, + model.PermissionManageBoardCards, + model.PermissionViewBoard, + model.PermissionManageBoardProperties, + } + + hasNotPermissionTo := []*mmModel.Permission{} + th.checkBoardPermissions("elevated-admin", member, teamID, hasPermissionTo, hasNotPermissionTo) + }) } diff --git a/webapp/src/components/shareBoard/__snapshots__/shareBoard.test.tsx.snap b/webapp/src/components/shareBoard/__snapshots__/shareBoard.test.tsx.snap index 78765f630..7e76ccb8b 100644 --- a/webapp/src/components/shareBoard/__snapshots__/shareBoard.test.tsx.snap +++ b/webapp/src/components/shareBoard/__snapshots__/shareBoard.test.tsx.snap @@ -1991,6 +1991,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select > @username_1 +
+
+ Team Admin +
+
@@ -2017,6 +2026,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select > @username_2 +
+
+ Admin +
+
diff --git a/webapp/src/components/shareBoard/__snapshots__/userPermissionsRow.test.tsx.snap b/webapp/src/components/shareBoard/__snapshots__/userPermissionsRow.test.tsx.snap index eeae83108..8745f3ea7 100644 --- a/webapp/src/components/shareBoard/__snapshots__/userPermissionsRow.test.tsx.snap +++ b/webapp/src/components/shareBoard/__snapshots__/userPermissionsRow.test.tsx.snap @@ -728,3 +728,263 @@ exports[`src/components/shareBoard/userPermissionsRow should match snapshot in t `; + +exports[`src/components/shareBoard/userPermissionsRow should match snapshot-admin 1`] = ` +
+
+
+
+ + + @username_1 + + + (You) + +
+
+ Admin +
+
+
+
+
+
@@ -1213,9 +1213,9 @@ exports[`components/sidebarSidebar some categories hidden 1`] = ` >
- v7.9.0 + v7.10.0
diff --git a/webapp/src/constants.ts b/webapp/src/constants.ts index d900092b1..9331eebe4 100644 --- a/webapp/src/constants.ts +++ b/webapp/src/constants.ts @@ -37,8 +37,8 @@ class Constants { static readonly titleColumnId = '__title' static readonly badgesColumnId = '__badges' - static readonly versionString = '7.9.0' - static readonly versionDisplayString = 'Mar 2023' + static readonly versionString = '7.10.0' + static readonly versionDisplayString = 'Apr 2023' static readonly archiveHelpPage = 'https://docs.mattermost.com/boards/migrate-to-boards.html' static readonly imports = [ From 39dbdbba7721ba5bd589c83c01a5fa2a620a1afb Mon Sep 17 00:00:00 2001 From: Harshil Sharma Date: Fri, 3 Mar 2023 15:10:23 +0530 Subject: [PATCH 42/56] Added data migration to de-duplicate data from category_boards table --- .../store/sqlstore/data_migrations.go | 96 ++++++++++++++++++- server/services/store/sqlstore/migrate.go | 10 ++ ...ategory_board_add_unique_constraint.up.sql | 2 +- 3 files changed, 102 insertions(+), 6 deletions(-) diff --git a/server/services/store/sqlstore/data_migrations.go b/server/services/store/sqlstore/data_migrations.go index ae1974792..a505cb97d 100644 --- a/server/services/store/sqlstore/data_migrations.go +++ b/server/services/store/sqlstore/data_migrations.go @@ -21,11 +21,12 @@ const ( // query, so we want to stay safely below. CategoryInsertBatch = 1000 - TemplatesToTeamsMigrationKey = "TemplatesToTeamsMigrationComplete" - UniqueIDsMigrationKey = "UniqueIDsMigrationComplete" - CategoryUUIDIDMigrationKey = "CategoryUuidIdMigrationComplete" - TeamLessBoardsMigrationKey = "TeamLessBoardsMigrationComplete" - DeletedMembershipBoardsMigrationKey = "DeletedMembershipBoardsMigrationComplete" + TemplatesToTeamsMigrationKey = "TemplatesToTeamsMigrationComplete" + UniqueIDsMigrationKey = "UniqueIDsMigrationComplete" + CategoryUUIDIDMigrationKey = "CategoryUuidIdMigrationComplete" + TeamLessBoardsMigrationKey = "TeamLessBoardsMigrationComplete" + DeletedMembershipBoardsMigrationKey = "DeletedMembershipBoardsMigrationComplete" + DeDuplicateCategoryBoardTableMigrationKey = "DeDuplicateCategoryBoardTableComplete" ) func (s *SQLStore) getBlocksWithSameID(db sq.BaseRunner) ([]*model.Block, error) { @@ -790,3 +791,88 @@ func (s *SQLStore) getCollationAndCharset(tableName string) (string, string, err return collation, charSet, nil } + +func (s *SQLStore) RunDeDuplicateCategoryBoardsMigration(currentMigration int) error { + setting, err := s.GetSystemSetting(DeDuplicateCategoryBoardTableMigrationKey) + if err != nil { + return fmt.Errorf("cannot get DeDuplicateCategoryBoardTableMigration state: %w", err) + } + + // If the migration is already completed, do not run it again. + if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun { + return nil + } + + if currentMigration >= (deDuplicateCategoryBoards + 1) { + // if the migration for which we're fixing the data is already applied, + // no need to check fix anything + + if err := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); err != nil { + return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", err) + } + return nil + } + + needed, err := s.doesDuplicateCategoryBoardsExist() + if err != nil { + return err + } + + if !needed { + if err := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); err != nil { + return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", err) + } + } + + if s.dbType == model.MysqlDBType { + return s.runMySQLDeDuplicateCategoryBoardsMigration() + } else if s.dbType == model.PostgresDBType { + return s.runPostgresDeDuplicateCategoryBoardsMigration() + } + + if err := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); err != nil { + return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", err) + } + + return nil +} + +func (s *SQLStore) doesDuplicateCategoryBoardsExist() (bool, error) { + subQuery := s.getQueryBuilder(s.db). + Select("user_id", "board_id", "count(*) AS count"). + From(s.tablePrefix+"category_boards"). + GroupBy("user_id", "board_id"). + Having("count(*) > 1") + + query := s.getQueryBuilder(s.db). + Select("COUNT(*)"). + FromSelect(subQuery, "duplicate_dataset") + + row := query.QueryRow() + + count := 0 + if err := row.Scan(&count); err != nil { + s.logger.Error("Error occurred reading number of duplicate records in category_boards table", mlog.Err(err)) + return false, err + } + + return count > 0, nil +} + +func (s *SQLStore) runMySQLDeDuplicateCategoryBoardsMigration() error { + query := fmt.Sprintf("WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum FROM %[1]scategory_boards) DELETE %[1]scategory_boards FROM %[1]scategory_boards JOIN duplicates USING(id) WHERE duplicates.rownum > 1;", s.tablePrefix) + if _, err := s.db.Exec(query); err != nil { + s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err)) + } + + return nil +} + +func (s *SQLStore) runPostgresDeDuplicateCategoryBoardsMigration() error { + query := fmt.Sprintf("WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum FROM %[1]scategory_boards) DELETE FROM %[1]scategory_boards USING duplicates WHERE %[1]scategory_boards.id = duplicates.id AND duplicates.rownum > 1;", s.tablePrefix) + if _, err := s.db.Exec(query); err != nil { + s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err)) + } + + return nil +} diff --git a/server/services/store/sqlstore/migrate.go b/server/services/store/sqlstore/migrate.go index afd9463ac..dd1be4d5f 100644 --- a/server/services/store/sqlstore/migrate.go +++ b/server/services/store/sqlstore/migrate.go @@ -36,6 +36,7 @@ const ( uniqueIDsMigrationRequiredVersion = 14 teamLessBoardsMigrationRequiredVersion = 18 categoriesUUIDIDMigrationRequiredVersion = 20 + deDuplicateCategoryBoards = 35 tempSchemaMigrationTableName = "temp_schema_migration" ) @@ -248,6 +249,15 @@ func (s *SQLStore) runMigrationSequence(engine *morph.Morph, driver drivers.Driv return err } + if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, deDuplicateCategoryBoards); mErr != nil { + return mErr + } + + currentMigrationVersion := len(appliedMigrations) + if mErr := s.RunDeDuplicateCategoryBoardsMigration(currentMigrationVersion); err != nil { + return mErr + } + s.logger.Debug("== Applying all remaining migrations ====================", mlog.Int("current_version", len(appliedMigrations)), ) diff --git a/server/services/store/sqlstore/migrations/000036_category_board_add_unique_constraint.up.sql b/server/services/store/sqlstore/migrations/000036_category_board_add_unique_constraint.up.sql index 8858033b0..4a658bdb3 100644 --- a/server/services/store/sqlstore/migrations/000036_category_board_add_unique_constraint.up.sql +++ b/server/services/store/sqlstore/migrations/000036_category_board_add_unique_constraint.up.sql @@ -23,4 +23,4 @@ SELECT id, user_id, category_id, board_id, create_at, update_at, sort_order, hidden FROM {{.prefix}}category_boards_old; DROP TABLE {{.prefix}}category_boards_old; -{{end}} \ No newline at end of file +{{end}} From 7195a2bf1d9e10210f0d2ae137b5e78df984a490 Mon Sep 17 00:00:00 2001 From: Harshil Sharma Date: Fri, 3 Mar 2023 15:45:24 +0530 Subject: [PATCH 43/56] Lint fix --- mattermost-plugin/server/manifest.go | 3 +-- .../store/sqlstore/data_migrations.go | 22 ++++++++++++------- server/services/store/sqlstore/migrate.go | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/mattermost-plugin/server/manifest.go b/mattermost-plugin/server/manifest.go index db79def9f..941134c75 100644 --- a/mattermost-plugin/server/manifest.go +++ b/mattermost-plugin/server/manifest.go @@ -45,8 +45,7 @@ const manifestStr = ` "type": "bool", "help_text": "This allows board editors to share boards that can be accessed by anyone with the link.", "placeholder": "", - "default": false, - "hosting": "" + "default": false } ] } diff --git a/server/services/store/sqlstore/data_migrations.go b/server/services/store/sqlstore/data_migrations.go index a505cb97d..8d0a3dad3 100644 --- a/server/services/store/sqlstore/data_migrations.go +++ b/server/services/store/sqlstore/data_migrations.go @@ -807,8 +807,8 @@ func (s *SQLStore) RunDeDuplicateCategoryBoardsMigration(currentMigration int) e // if the migration for which we're fixing the data is already applied, // no need to check fix anything - if err := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); err != nil { - return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", err) + if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil { + return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr) } return nil } @@ -819,8 +819,8 @@ func (s *SQLStore) RunDeDuplicateCategoryBoardsMigration(currentMigration int) e } if !needed { - if err := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); err != nil { - return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", err) + if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil { + return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr) } } @@ -830,8 +830,8 @@ func (s *SQLStore) RunDeDuplicateCategoryBoardsMigration(currentMigration int) e return s.runPostgresDeDuplicateCategoryBoardsMigration() } - if err := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); err != nil { - return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", err) + if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil { + return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr) } return nil @@ -860,7 +860,10 @@ func (s *SQLStore) doesDuplicateCategoryBoardsExist() (bool, error) { } func (s *SQLStore) runMySQLDeDuplicateCategoryBoardsMigration() error { - query := fmt.Sprintf("WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum FROM %[1]scategory_boards) DELETE %[1]scategory_boards FROM %[1]scategory_boards JOIN duplicates USING(id) WHERE duplicates.rownum > 1;", s.tablePrefix) + query := "WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum " + + "FROM " + s.tablePrefix + "category_boards) " + + "DELETE " + s.tablePrefix + "category_boards FROM " + s.tablePrefix + "category_boards " + + "JOIN duplicates USING(id) WHERE duplicates.rownum > 1;" if _, err := s.db.Exec(query); err != nil { s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err)) } @@ -869,7 +872,10 @@ func (s *SQLStore) runMySQLDeDuplicateCategoryBoardsMigration() error { } func (s *SQLStore) runPostgresDeDuplicateCategoryBoardsMigration() error { - query := fmt.Sprintf("WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum FROM %[1]scategory_boards) DELETE FROM %[1]scategory_boards USING duplicates WHERE %[1]scategory_boards.id = duplicates.id AND duplicates.rownum > 1;", s.tablePrefix) + query := "WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum " + + "FROM " + s.tablePrefix + "category_boards) " + + "DELETE FROM " + s.tablePrefix + "category_boards USING duplicates " + + "WHERE " + s.tablePrefix + "category_boards.id = duplicates.id AND duplicates.rownum > 1;" if _, err := s.db.Exec(query); err != nil { s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err)) } diff --git a/server/services/store/sqlstore/migrate.go b/server/services/store/sqlstore/migrate.go index dd1be4d5f..d3fd7e3c5 100644 --- a/server/services/store/sqlstore/migrate.go +++ b/server/services/store/sqlstore/migrate.go @@ -254,7 +254,7 @@ func (s *SQLStore) runMigrationSequence(engine *morph.Morph, driver drivers.Driv } currentMigrationVersion := len(appliedMigrations) - if mErr := s.RunDeDuplicateCategoryBoardsMigration(currentMigrationVersion); err != nil { + if mErr := s.RunDeDuplicateCategoryBoardsMigration(currentMigrationVersion); mErr != nil { return mErr } From 1e3c03354724425b4448faf1ee40fd4e702a4a25 Mon Sep 17 00:00:00 2001 From: Harshil Sharma Date: Fri, 3 Mar 2023 15:55:52 +0530 Subject: [PATCH 44/56] shortcircuit for mysql --- server/services/store/sqlstore/data_migrations.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/services/store/sqlstore/data_migrations.go b/server/services/store/sqlstore/data_migrations.go index 8d0a3dad3..15d86129d 100644 --- a/server/services/store/sqlstore/data_migrations.go +++ b/server/services/store/sqlstore/data_migrations.go @@ -793,6 +793,14 @@ func (s *SQLStore) getCollationAndCharset(tableName string) (string, string, err } func (s *SQLStore) RunDeDuplicateCategoryBoardsMigration(currentMigration int) error { + // not supported for SQLite + if s.dbType == model.SqliteDBType { + if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil { + return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr) + } + return nil + } + setting, err := s.GetSystemSetting(DeDuplicateCategoryBoardTableMigrationKey) if err != nil { return fmt.Errorf("cannot get DeDuplicateCategoryBoardTableMigration state: %w", err) From f9aa6c1668a7f3dd91c89e2f720a5c9958cb312a Mon Sep 17 00:00:00 2001 From: Harshil Sharma Date: Fri, 3 Mar 2023 18:49:53 +0530 Subject: [PATCH 45/56] Fixed server test --- server/services/store/storetests/system.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/services/store/storetests/system.go b/server/services/store/storetests/system.go index d6fa83a99..6a59826aa 100644 --- a/server/services/store/storetests/system.go +++ b/server/services/store/storetests/system.go @@ -10,8 +10,9 @@ import ( // these system settings are created when running the data migrations, // so they will be present after the tests setup. var dataMigrationSystemSettings = map[string]string{ - "UniqueIDsMigrationComplete": "true", - "CategoryUuidIdMigrationComplete": "true", + "UniqueIDsMigrationComplete": "true", + "CategoryUuidIdMigrationComplete": "true", + "DeDuplicateCategoryBoardTableComplete": "true", } func addBaseSettings(m map[string]string) map[string]string { From 56954e0491844a956ac095f63b97d72b68aa2523 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Sat, 4 Mar 2023 12:20:05 +0500 Subject: [PATCH 46/56] Updating welcome screen for small sizes (#4609) --- .../__snapshots__/welcomePage.test.tsx.snap | 112 ++++++++++-------- webapp/src/pages/welcome/welcomePage.scss | 21 +++- webapp/src/pages/welcome/welcomePage.tsx | 104 ++++++++-------- 3 files changed, 138 insertions(+), 99 deletions(-) diff --git a/webapp/src/pages/welcome/__snapshots__/welcomePage.test.tsx.snap b/webapp/src/pages/welcome/__snapshots__/welcomePage.test.tsx.snap index 2dfc52f90..ef9ac5df4 100644 --- a/webapp/src/pages/welcome/__snapshots__/welcomePage.test.tsx.snap +++ b/webapp/src/pages/welcome/__snapshots__/welcomePage.test.tsx.snap @@ -18,32 +18,40 @@ exports[`pages/welcome Welcome Page shows Explore Page 1`] = ` > Boards is a project management tool that helps define, organize, track, and manage work across teams using a familiar Kanban board view. - Boards Welcome Image - Boards Welcome Image - - + Boards Welcome Image +
+ + +
+ @@ -67,32 +75,40 @@ exports[`pages/welcome Welcome Page shows Explore Page with subpath 1`] = ` > Boards is a project management tool that helps define, organize, track, and manage work across teams using a familiar Kanban board view. - Boards Welcome Image - Boards Welcome Image - - + Boards Welcome Image +
+ + +
+ diff --git a/webapp/src/pages/welcome/welcomePage.scss b/webapp/src/pages/welcome/welcomePage.scss index d6810e1bf..ebeb7f2b6 100644 --- a/webapp/src/pages/welcome/welcomePage.scss +++ b/webapp/src/pages/welcome/welcomePage.scss @@ -10,6 +10,26 @@ @media (max-height: 768px) { justify-content: flex-start; height: auto; + padding-top: 40px; + } + + .WelcomePage__content { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + + @media (max-height: 800px) { + flex-direction: column-reverse; + margin-top: 16px; + } + } + + .WelcomePage__buttons { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; } > div { @@ -34,7 +54,6 @@ } .skip { - margin-top: 12px; color: rgba(var(--link-color-rgb), 1); cursor: pointer; diff --git a/webapp/src/pages/welcome/welcomePage.tsx b/webapp/src/pages/welcome/welcomePage.tsx index df7a6edbf..723f5c3f5 100644 --- a/webapp/src/pages/welcome/welcomePage.tsx +++ b/webapp/src/pages/welcome/welcomePage.tsx @@ -127,59 +127,63 @@ const WelcomePage = () => { /> - {/* This image will be rendered on large screens over 2000px */} - Boards Welcome Image +
+ {/* This image will be rendered on large screens over 2000px */} + Boards Welcome Image - {/* This image will be rendered on small screens below 2000px */} - Boards Welcome Image + {/* This image will be rendered on small screens below 2000px */} + Boards Welcome Image - {me?.is_guest !== true && - } +
+ {me?.is_guest !== true && + } - {me?.is_guest !== true && - - - } - {me?.is_guest === true && - } + {me?.is_guest !== true && + + + } + {me?.is_guest === true && + } +
+
) From 24d6ad48e7cc7b35daba309dc7993bb4103711f7 Mon Sep 17 00:00:00 2001 From: Vidar Haarr Date: Fri, 3 Mar 2023 14:25:16 +0100 Subject: [PATCH 47/56] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 40.6% (183 of 450 strings) Translation: Focalboard/webapp Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/nb_NO/ --- webapp/i18n/nb_NO.json | 169 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 164 insertions(+), 5 deletions(-) diff --git a/webapp/i18n/nb_NO.json b/webapp/i18n/nb_NO.json index 3b6c0ce38..604a40885 100644 --- a/webapp/i18n/nb_NO.json +++ b/webapp/i18n/nb_NO.json @@ -1,14 +1,42 @@ { + "AppBar.Tooltip": "Veksle lenkede tavler", + "Attachment.Attachment-title": "Vedlegg", + "AttachmentBlock.DeleteAction": "slett", + "AttachmentBlock.addElement": "legg til {type}", + "AttachmentBlock.delete": "Vedlegg slettet.", + "AttachmentBlock.failed": "Denne filen kunne ikke lastes opp fordi størrelsesgrensen er nådd.", + "AttachmentBlock.upload": "Vedlegg lastes opp.", + "AttachmentBlock.uploadSuccess": "Vedlegg lastet opp.", + "AttachmentElement.delete-confirmation-dialog-button-text": "Slett", + "AttachmentElement.download": "Last ned", + "AttachmentElement.upload-percentage": "Laster opp ...({uploadPercent}%)", "BoardComponent.add-a-group": "+ Legg til gruppe", "BoardComponent.delete": "Slett", "BoardComponent.hidden-columns": "Skjulte kolonner", "BoardComponent.hide": "Skjul", "BoardComponent.new": "+ Ny", "BoardComponent.no-property": "Ingen {property}", - "BoardComponent.no-property-title": "Elementer med en tom {property} område kommer hit. Denne kolonnen kan ikke fjernes.", + "BoardComponent.no-property-title": "Elementer med tom {property} atributt legges her. Denne kolonnen kan ikke fjernes.", "BoardComponent.show": "Vis", + "BoardMember.schemeAdmin": "Admin", + "BoardMember.schemeCommenter": "Kommentator", + "BoardMember.schemeEditor": "Redaktør", + "BoardMember.schemeNone": "Ingen", + "BoardMember.schemeViewer": "Viser", + "BoardMember.unlinkChannel": "Fjern lenke", "BoardPage.newVersion": "En ny versjon av Boards er tilgjengelig, klikk her for å laste inn på nytt.", "BoardPage.syncFailed": "Tavle kan slettes eller adgangen trekkes tilbake.", + "BoardTemplateSelector.add-template": "Lag ny mal", + "BoardTemplateSelector.create-empty-board": "Opprett tom tavle", + "BoardTemplateSelector.delete-template": "Slett", + "BoardTemplateSelector.description": "Legg til en tavle til sidestolpen med hvilken mal du vil fra listen under, eller start med en helt tom tavle.", + "BoardTemplateSelector.edit-template": "Rediger", + "BoardTemplateSelector.plugin.no-content-description": "Legg til en tavle i sidestolpen med hvilken mal du vil, eller start med en tom tavle.", + "BoardTemplateSelector.plugin.no-content-title": "Lag ny tavle", + "BoardTemplateSelector.title": "Lag ny tavle", + "BoardTemplateSelector.use-this-template": "Bruk denne malen", + "BoardsSwitcher.Title": "Finn tavle", + "BoardsUnfurl.Limited": "Flere detaljer er skjult fordi kortet er arkivert", "BoardsUnfurl.Remainder": "+{remainder} mer", "BoardsUnfurl.Updated": "Oppdatert {time}", "Calculations.Options.average.displayName": "Gjennomsnitt", @@ -16,11 +44,142 @@ "Calculations.Options.count.displayName": "Antall", "Calculations.Options.count.label": "Antall", "Calculations.Options.countChecked.displayName": "Avkrysset", - "Calculations.Options.countChecked.label": "Antall avsjekket", + "Calculations.Options.countChecked.label": "Antall valgt", "Calculations.Options.countUnchecked.displayName": "Ikke avmerket", - "Calculations.Options.countUnchecked.label": "Antall Ikke Avmerket", + "Calculations.Options.countUnchecked.label": "Antall ikke valgt", "Calculations.Options.countUniqueValue.displayName": "Unik", - "Calculations.Options.countUniqueValue.label": "Antall Unike Verdier", + "Calculations.Options.countUniqueValue.label": "Antall unike verdier", "Calculations.Options.countValue.displayName": "Verdier", - "Calculations.Options.countValue.label": "Antall Verdier" + "Calculations.Options.countValue.label": "Antall verdier", + "Calculations.Options.dateRange.displayName": "Tidsrom", + "Calculations.Options.dateRange.label": "Tidsrom", + "Calculations.Options.earliest.displayName": "Tiligst", + "Calculations.Options.earliest.label": "Tiligst", + "Calculations.Options.latest.displayName": "Senest", + "Calculations.Options.latest.label": "Senest", + "Calculations.Options.max.displayName": "Maks", + "Calculations.Options.max.label": "Maks", + "Calculations.Options.median.displayName": "Median", + "Calculations.Options.median.label": "Median", + "Calculations.Options.min.displayName": "Min", + "Calculations.Options.min.label": "Min", + "Calculations.Options.none.displayName": "Kalkulèr", + "Calculations.Options.none.label": "Ingen", + "Calculations.Options.percentChecked.displayName": "Valgt", + "Calculations.Options.percentChecked.label": "Prosent valgt", + "Calculations.Options.percentUnchecked.displayName": "Ikke valgt", + "Calculations.Options.percentUnchecked.label": "Prosent ikke valgt", + "Calculations.Options.range.displayName": "Tidsrom", + "Calculations.Options.range.label": "Tidsrom", + "Calculations.Options.sum.displayName": "Sum", + "Calculations.Options.sum.label": "Sum", + "CalendarCard.untitled": "Uten navn", + "CardActionsMenu.copiedLink": "Kopiert!", + "CardActionsMenu.copyLink": "Kopier lenke", + "CardActionsMenu.delete": "Slett", + "CardActionsMenu.duplicate": "Dupliser", + "CardBadges.title-checkboxes": "Avkrysningsbokser", + "CardBadges.title-comments": "Kommentarer", + "CardBadges.title-description": "Dette kortet har en beskrivelsestekst", + "CardDetail.Attach": "Legg ved", + "CardDetail.Follow": "Følg", + "CardDetail.Following": "Følger", + "CardDetail.add-content": "Legg til innhold", + "CardDetail.add-icon": "Legg til ikon", + "CardDetail.add-property": "+ Legg til en verdi", + "CardDetail.addCardText": "legg inn tekst i kortet", + "CardDetail.limited-body": "Oppgrader til vår profesjonelle eller bedriftsplan.", + "CardDetail.limited-button": "Oppgrader", + "CardDetail.limited-title": "Dette kortet er skjult", + "CardDetail.moveContent": "Flytt innholdet", + "CardDetail.new-comment-placeholder": "Legg til kommentar ...", + "CardDetailProperty.confirm-delete-heading": "Bekreft sletting av verdi", + "CardDetailProperty.confirm-delete-subtext": "Er du sikker på at du vil slette verdien \"{propertyName}\"? Dette vil fjerne verdien fra alle kortene på denne tavlen.", + "CardDetailProperty.confirm-property-name-change-subtext": "Er du sikker på at du vil endre verdien \"{propertyName}\" {customText}? Dette vil påvirke verdien på {numOfCards} kort på denne tavlen, og kan forårsake at du mister informasjon.", + "CardDetailProperty.confirm-property-type-change": "Bekreft endring av verditype", + "CardDetailProperty.delete-action-button": "Slett", + "CardDetailProperty.property-change-action-button": "Endre verdi", + "CardDetailProperty.property-changed": "Verdi endret!", + "CardDetailProperty.property-deleted": "Fjernet {propertyName}!", + "CardDetailProperty.property-name-change-subtext": "type fra \"{oldPropType}\" til \"{newPropType}\"", + "CardDetial.limited-link": "Lær mer om våre planer.", + "CardDialog.delete-confirmation-dialog-attachment": "Bekreft sletting av vedlegg", + "CardDialog.delete-confirmation-dialog-button-text": "Slett", + "CardDialog.delete-confirmation-dialog-heading": "Bekreft sletting av kort", + "CardDialog.editing-template": "Du redigerer en mal.", + "CardDialog.nocard": "Dette kortet eksisterer ikke eller du har ikke tilgang.", + "Categories.CreateCategoryDialog.CancelText": "Avbryt", + "Categories.CreateCategoryDialog.CreateText": "Opprett", + "Categories.CreateCategoryDialog.Placeholder": "Navngi kategorien", + "Categories.CreateCategoryDialog.UpdateText": "Oppdater", + "CenterPanel.Login": "Logg inn", + "CenterPanel.Share": "Del", + "ChannelIntro.CreateBoard": "Opprett tavle", + "CloudMessage.cloud-server": "Få din egen gratis skytjener.", + "ColorOption.selectColor": "Velg {color} farge", + "Comment.delete": "Slett", + "CommentsList.send": "Send", + "ConfirmPerson.empty": "Tom", + "ConfirmPerson.search": "Søk ...", + "ConfirmationDialog.cancel-action": "Avbryt", + "ConfirmationDialog.confirm-action": "Bekreft", + "ContentBlock.Delete": "Slett", + "ContentBlock.DeleteAction": "slett", + "ContentBlock.addElement": "legg til {type}", + "ContentBlock.checkbox": "avkrysningsboks", + "ContentBlock.divider": "avdeler", + "ContentBlock.editCardCheckbox": "krysset-boks", + "ContentBlock.editCardCheckboxText": "rediger kort tekst", + "ContentBlock.editCardText": "rediger kort tekst", + "ContentBlock.editText": "Rediger tekst ...", + "ContentBlock.image": "bilde", + "ContentBlock.insertAbove": "Sett inn over", + "ContentBlock.moveBlock": "flytt kort innhold", + "ContentBlock.moveDown": "Flytt ned", + "ContentBlock.moveUp": "Flytt opp", + "ContentBlock.text": "tekst", + "DateRange.clear": "Tøm", + "DateRange.empty": "Tom", + "DateRange.endDate": "Sluttdato", + "DateRange.today": "I dag", + "DeleteBoardDialog.confirm-cancel": "Avbryt", + "DeleteBoardDialog.confirm-delete": "Slett", + "DeleteBoardDialog.confirm-info": "Er du sikker på at du vil slette tavlen \"{boardTitle}\"? Dette vil slette alle kortene på tavlen.", + "DeleteBoardDialog.confirm-info-template": "Er du sikker på at du vil slette tavlemalen \"{boardTitle}\"?", + "DeleteBoardDialog.confirm-tite": "Bekreft sletting av tavle", + "DeleteBoardDialog.confirm-tite-template": "Bekreft sletting av tavlemal", + "Dialog.closeDialog": "Lukk", + "EditableDayPicker.today": "I dag", + "Error.mobileweb": "Støtte for bruk i nettleser på mobil er i tidlig beta. Alt vil ikke fungere.", + "Error.websocket-closed": "Problemer med kobling til tjeneren. Sjekk konfigurasjonen hvis problemet vedvarer.", + "Filter.contains": "inneholder", + "Filter.ends-with": "ender med", + "Filter.includes": "inkluderer", + "Filter.is": "er", + "Filter.is-empty": "er tom", + "Filter.is-not-empty": "er ikke tom", + "Filter.is-not-set": "er ikke satt", + "Filter.is-set": "er satt", + "Filter.not-contains": "inkluderer ikke", + "Filter.not-ends-with": "ender ikke med", + "Filter.not-includes": "inkluderer ikke", + "Filter.not-starts-with": "starter ikke med", + "Filter.starts-with": "starter med", + "FilterByText.placeholder": "filtrer tekst", + "FilterComponent.add-filter": "+ Nytt filter", + "FilterComponent.delete": "Slett", + "FilterValue.empty": "(tom)", + "FindBoardsDialog.IntroText": "Søk etter tavle", + "FindBoardsDialog.NoResultsFor": "Ingen resultat for \"{searchQuery}\"", + "FindBoardsDialog.NoResultsSubtext": "Sjekk stavingen eller søk på noe annet.", + "FindBoardsDialog.SubTitle": "Skriv for å finne en tavle. Bruk opp/ned for å navigere. Enter for å velge, eller Esc for å avbryte", + "FindBoardsDialog.Title": "Finn tavle", + "GroupBy.hideEmptyGroups": "Skjul {count} tomme grupper", + "GroupBy.showHiddenGroups": "Vis {count} tomme grupper", + "GroupBy.ungroup": "Fjern fra gruppe", + "HideBoard.MenuOption": "Skjul tavlen", + "KanbanCard.untitled": "Uten navn", + "Mutator.new-board-from-template": "ny tavle fra mal", + "Mutator.new-card-from-template": "nytt kort fra mal", + "Mutator.new-template-from-card": "ny mal fra kort" } From 8fe781a16cbe7231372c48b741b9955e64ee598f Mon Sep 17 00:00:00 2001 From: Dmitry Scherbak Date: Fri, 3 Mar 2023 14:25:16 +0100 Subject: [PATCH 48/56] Translated using Weblate (Russian) Currently translated at 75.7% (341 of 450 strings) Translation: Focalboard/webapp Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/ru/ --- webapp/i18n/ru.json | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/webapp/i18n/ru.json b/webapp/i18n/ru.json index b2a7419f8..f6b502b65 100644 --- a/webapp/i18n/ru.json +++ b/webapp/i18n/ru.json @@ -2,14 +2,14 @@ "AppBar.Tooltip": "Переключить связанные доски", "Attachment.Attachment-title": "Вложение", "AttachmentBlock.DeleteAction": "Удалить", - "AttachmentBlock.addElement": "добавить", - "AttachmentBlock.delete": "Вложение успешно удалено.", - "AttachmentBlock.failed": "Не удалось загрузить файл. Достигнут предел размера вложения.", + "AttachmentBlock.addElement": "добавить {type}", + "AttachmentBlock.delete": "Вложение удалено.", + "AttachmentBlock.failed": "Не удалось загрузить файл, так как превышена квота на размер файла.", "AttachmentBlock.upload": "Загрузка вложения.", - "AttachmentBlock.uploadSuccess": "Вложение успешно загружено.", + "AttachmentBlock.uploadSuccess": "Вложение загружено.", "AttachmentElement.delete-confirmation-dialog-button-text": "Удалить", "AttachmentElement.download": "Скачать", - "AttachmentElement.upload-percentage": "Загрузка", + "AttachmentElement.upload-percentage": "Загрузка...({uploadPercent}%)", "BoardComponent.add-a-group": "+ Добавить группу", "BoardComponent.delete": "Удалить", "BoardComponent.hidden-columns": "Скрытые столбцы", @@ -31,12 +31,12 @@ "BoardTemplateSelector.delete-template": "Удалить", "BoardTemplateSelector.description": "Добавьте доску на боковую панель, используя любой из шаблонов, описанных ниже, или начните с нуля.", "BoardTemplateSelector.edit-template": "Изменить", - "BoardTemplateSelector.plugin.no-content-description": "Добавьте доску на боковую панель, используя любой из указанных ниже шаблонов, или начните с нуля.{lineBreak} Участники \"{teamName}\" будут иметь доступ к созданным здесь доскам.", + "BoardTemplateSelector.plugin.no-content-description": "Добавьте доску на боковую панель, используя любой из указанных ниже шаблонов, или начните с нуля.", "BoardTemplateSelector.plugin.no-content-title": "Создать доску", "BoardTemplateSelector.title": "Создать доску", "BoardTemplateSelector.use-this-template": "Использовать этот шаблон", "BoardsSwitcher.Title": "Найти доски", - "BoardsUnfurl.Limited": "Информация скрыта в связи с тем, что карточка находится в архиве", + "BoardsUnfurl.Limited": "Информация скрыта, потому что карточка находится в архиве", "BoardsUnfurl.Remainder": "+{remainder} ещё", "BoardsUnfurl.Updated": "Обновлено {time}", "Calculations.Options.average.displayName": "Среднее", @@ -103,9 +103,9 @@ "CardDetailProperty.property-deleted": "{propertyName} успешно удалено!", "CardDetailProperty.property-name-change-subtext": "тип из \"{oldPropType}\" в \"{newPropType}\"", "CardDetial.limited-link": "Узнайте больше о наших планах.", - "CardDialog.delete-confirmation-dialog-attachment": "Подтвердите удаление вложения!", + "CardDialog.delete-confirmation-dialog-attachment": "Подтвердите удаление вложения", "CardDialog.delete-confirmation-dialog-button-text": "Удалить", - "CardDialog.delete-confirmation-dialog-heading": "Подтвердите удаление карточки!", + "CardDialog.delete-confirmation-dialog-heading": "Подтвердите удаление карточки", "CardDialog.editing-template": "Вы редактируете шаблон.", "CardDialog.nocard": "Эта карточка не существует или недоступна.", "Categories.CreateCategoryDialog.CancelText": "Отмена", @@ -114,10 +114,13 @@ "Categories.CreateCategoryDialog.UpdateText": "Обновить", "CenterPanel.Login": "Логин", "CenterPanel.Share": "Поделиться", + "ChannelIntro.CreateBoard": "Создать доску", "CloudMessage.cloud-server": "Получите свой бесплатный облачный сервер.", "ColorOption.selectColor": "Выберите цвет {color}", "Comment.delete": "Удалить", "CommentsList.send": "Отправить", + "ConfirmPerson.empty": "Пусто", + "ConfirmPerson.search": "Поиск...", "ConfirmationDialog.cancel-action": "Отмена", "ConfirmationDialog.confirm-action": "Подтвердить", "ContentBlock.Delete": "Удалить", @@ -165,6 +168,7 @@ "FilterByText.placeholder": "фильтровать текст", "FilterComponent.add-filter": "+ Добавить фильтр", "FilterComponent.delete": "Удалить", + "FilterValue.empty": "(пусто)", "FindBoardsDialog.IntroText": "Поиск досок", "FindBoardsDialog.NoResultsFor": "Нет результатов для \"{searchQuery}\"", "FindBoardsDialog.NoResultsSubtext": "Проверьте правильность написания или попробуйте другой запрос.", @@ -183,7 +187,7 @@ "OnboardingTour.AddComments.Title": "Добавить комментарии", "OnboardingTour.AddDescription.Body": "Добавьте описание к своей карточке, чтобы Ваши коллеги по команде знали, о чем эта карточка.", "OnboardingTour.AddDescription.Title": "Добавить описание", - "OnboardingTour.AddProperties.Body": "Добавляйте различные свойства карточкам, чтобы сделать их более мощными!", + "OnboardingTour.AddProperties.Body": "Добавляйте различные свойства карточкам, чтобы сделать их более значительными.", "OnboardingTour.AddProperties.Title": "Добавить свойства", "OnboardingTour.AddView.Body": "Перейдите сюда, чтобы создать новый вид для организации доски с использованием различных макетов.", "OnboardingTour.AddView.Title": "Добавить новый вид", @@ -284,6 +288,7 @@ "TableHeaderMenu.insert-right": "Вставить справа", "TableHeaderMenu.sort-ascending": "Сортировать по возрастанию", "TableHeaderMenu.sort-descending": "Сортировать по убыванию", + "TableRow.DuplicateCard": "дублировать карточку", "TableRow.MoreOption": "Больше действий", "TableRow.open": "Открыть", "TopBar.give-feedback": "Дать обратную связь", @@ -351,10 +356,13 @@ "WelcomePage.Explore.Button": "Исследовать", "WelcomePage.Heading": "Добро пожаловать на Доски", "WelcomePage.NoThanks.Text": "Нет спасибо, сам разберусь", + "WelcomePage.StartUsingIt.Text": "Начать пользоваться", "Workspace.editing-board-template": "Вы редактируете шаблон доски.", + "badge.guest": "Гость", "boardSelector.confirm-link-board": "Привязать доску к каналу", "boardSelector.confirm-link-board-button": "Да, ссылка доски", - "boardSelector.confirm-link-board-subtext": "Связывание доски \"{boardName}\" с этим каналом даст всем участникам этого канала доступ на редактирование доски. Вы уверены, что хотите связать это?", + "boardSelector.confirm-link-board-subtext": "Связывание доски \"{boardName}\" с каналом даст всем участникам канала доступ на редактирование доски. Вы можете в любое время отвязать доску о канала.", + "boardSelector.confirm-link-board-subtext-with-other-channel": "Привязка \"{boardName}\" с каналом приведет к возможности её редактирования всеми участниками канала (существующими и новыми). Кроме гостей канала.{lineBreak} Эта доска сейчас связана с другим каналом. Он будет отключен, если вы решите изменить привязку.", "boardSelector.create-a-board": "Создать доску", "boardSelector.link": "Ссылка", "boardSelector.search-for-boards": "Поиск досок", @@ -363,6 +371,8 @@ "calendar.month": "Месяц", "calendar.today": "СЕГОДНЯ", "calendar.week": "Неделя", + "centerPanel.undefined": "Отсутствует {propertyName}", + "centerPanel.unknown-user": "Неизвестный пользователь", "cloudMessage.learn-more": "Учить больше", "createImageBlock.failed": "Не удалось загрузить файл. Достигнут предел размера файла.", "default-properties.badges": "Комментарии и описание", @@ -377,11 +387,15 @@ "error.team-undefined": "Не корректная команда.", "error.unknown": "Произошла ошибка.", "generic.previous": "Предыдущий", - "imagePaste.upload-failed": "Некоторые файлы не загружены. Достигнут предел размера файла", + "imagePaste.upload-failed": "Некоторые файлы не загружены из-за превышения квоты на размер файла.", "limitedCard.title": "Карточки скрыты", "login.log-in-button": "Вход в систему", "login.log-in-title": "Вход в систему", "login.register-button": "или создать аккаунт, если у Вас его нет", + "new_channel_modal.create_board.empty_board_description": "Создать новую пустую доску", + "new_channel_modal.create_board.empty_board_title": "Пустая доска", + "new_channel_modal.create_board.select_template_placeholder": "Выбрать шаблон", + "new_channel_modal.create_board.title": "Создать доску для этого канала", "notification-box-card-limit-reached.close-tooltip": "Отложить на 10 дней", "notification-box-card-limit-reached.contact-link": "уведомить Вашего администратора", "notification-box-card-limit-reached.link": "Перейти на платный тариф", @@ -389,13 +403,18 @@ "notification-box-cards-hidden.title": "Это действие скрыло другую карточку", "notification-box.card-limit-reached.not-admin.text": "Чтобы получить доступ к архивным карточкам, Вы можете {contactLink} перейти на платный тариф.", "notification-box.card-limit-reached.text": "Достигнут лимит карточки, чтобы просмотреть старые карточки, {link}", + "person.add-user-to-board": "Добавить {username} на доску", + "person.add-user-to-board-confirm-button": "Добавить доску", + "person.add-user-to-board-permissions": "Разрешения", + "person.add-user-to-board-question": "Вы хотите добавить {username} на доску?", "register.login-button": "или войти в систему, если у вас уже есть аккаунт", "register.signup-title": "Зарегистрируйте свой аккаунт", + "rhs-board-non-admin-msg": "Вы не являетесь администратором этой доски", "rhs-boards.add": "Добавить", "rhs-boards.last-update-at": "Последнее обновление: {datetime}", "rhs-boards.link-boards-to-channel": "Связать доски с {channelName}", "rhs-boards.linked-boards": "Связанные доски", - "rhs-boards.no-boards-linked-to-channel": "К каналу {channelName} пока не подключены доски.", + "rhs-boards.no-boards-linked-to-channel": "К каналу {channelName} пока не подключены доски", "rhs-boards.no-boards-linked-to-channel-description": "Доски — это инструмент управления проектами, который помогает определять, организовывать, отслеживать и управлять работой между командами, используя знакомое представление доски Канбан.", "rhs-boards.unlink-board": "Отвязать доску", "rhs-channel-boards-header.title": "Доски", From 6b5c263e5627ebbeeec4b14f83755d90308a148a Mon Sep 17 00:00:00 2001 From: Felipe Nogueira Date: Fri, 3 Mar 2023 14:25:16 +0100 Subject: [PATCH 49/56] Translated using Weblate (Portuguese (Brazil)) Currently translated at 94.2% (424 of 450 strings) Translation: Focalboard/webapp Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/pt_BR/ --- webapp/i18n/pt_BR.json | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/webapp/i18n/pt_BR.json b/webapp/i18n/pt_BR.json index 5cc319d38..aa755dc2f 100644 --- a/webapp/i18n/pt_BR.json +++ b/webapp/i18n/pt_BR.json @@ -1,5 +1,12 @@ { - "AppBar.Tooltip": "Ativar Boards vinculados", + "AppBar.Tooltip": "Ativar boards vinculados", + "Attachment.Attachment-title": "Anexo", + "AttachmentBlock.DeleteAction": "apagar", + "AttachmentBlock.addElement": "adicionar {type}", + "AttachmentBlock.delete": "Anexo apagado.", + "AttachmentBlock.uploadSuccess": "Anexo enviado.", + "AttachmentElement.delete-confirmation-dialog-button-text": "Apagar", + "AttachmentElement.upload-percentage": "Enviando...({uploadPercent}%)", "BoardComponent.add-a-group": "+ Adicione um grupo", "BoardComponent.delete": "Excluir", "BoardComponent.hidden-columns": "Colunas ocultas", @@ -16,8 +23,8 @@ "BoardMember.unlinkChannel": "Desvincular", "BoardPage.newVersion": "Uma nova versão do Boards está disponível, clique aqui para recarregar.", "BoardPage.syncFailed": "O Board pode ter sido excluído ou o acesso revogado.", - "BoardTemplateSelector.add-template": "Novo modelo", - "BoardTemplateSelector.create-empty-board": "Criar board vazio", + "BoardTemplateSelector.add-template": "Criar novo modelo", + "BoardTemplateSelector.create-empty-board": "Criar um board vazio", "BoardTemplateSelector.delete-template": "Excluir", "BoardTemplateSelector.description": "Adicione um quadro à barra lateral usando qualquer um dos modelos definidos abaixo ou comece do zero.", "BoardTemplateSelector.edit-template": "Editar", @@ -25,7 +32,7 @@ "BoardTemplateSelector.plugin.no-content-title": "Criar um board", "BoardTemplateSelector.title": "Criar um board", "BoardTemplateSelector.use-this-template": "Use este template", - "BoardsSwitcher.Title": "Encontrar Boards", + "BoardsSwitcher.Title": "Encontrar boards", "BoardsUnfurl.Limited": "Detalhes adicionais estão ocultos devido ao cartão ter sido arquivado", "BoardsUnfurl.Remainder": "+{remainder} mais", "BoardsUnfurl.Updated": "Atualizado {time}", @@ -71,6 +78,7 @@ "CardBadges.title-checkboxes": "Caixa de seleção", "CardBadges.title-comments": "Comentários", "CardBadges.title-description": "Este cartão tem uma descrição", + "CardDetail.Attach": "Anexar", "CardDetail.Follow": "Seguir", "CardDetail.Following": "Seguindo", "CardDetail.add-content": "Adicionar conteúdo", @@ -102,10 +110,13 @@ "Categories.CreateCategoryDialog.UpdateText": "Atualizar", "CenterPanel.Login": "Login", "CenterPanel.Share": "Compartilhar", + "ChannelIntro.CreateBoard": "Criar um board", "CloudMessage.cloud-server": "Obtenha seu próprio cloud server de graça.", "ColorOption.selectColor": "Selecione {color} Cor", "Comment.delete": "Excluir", "CommentsList.send": "Enviar", + "ConfirmPerson.empty": "Vazio", + "ConfirmPerson.search": "Buscar...", "ConfirmationDialog.cancel-action": "Cancelar", "ConfirmationDialog.confirm-action": "Confirmar", "ContentBlock.Delete": "Excluir", @@ -181,6 +192,7 @@ "OnboardingTour.ShareBoard.Body": "Você pode compartilhar seu board internament, com seu time, ou public para permitir visibilidade fora da sua organização.", "OnboardingTour.ShareBoard.Title": "Compartilhar quadro", "PersonProperty.board-members": "Membros do Board", + "PersonProperty.me": "Eu", "PersonProperty.non-board-members": "Não membros do board", "PropertyMenu.Delete": "Excluir", "PropertyMenu.changeType": "Alterar tipo da propriedade", @@ -235,6 +247,7 @@ "Sidebar.import-archive": "Importar arquivo", "Sidebar.invite-users": "Convidar usuários", "Sidebar.logout": "Sair", + "Sidebar.new-category.badge": "Novo", "Sidebar.no-boards-in-category": "Nenhum board", "Sidebar.product-tour": "Tour pelo produto", "Sidebar.random-icons": "Ícones aleatórios", @@ -257,6 +270,7 @@ "SidebarTour.SidebarCategories.Body": "Todos seus boards agora são organizados sob sua nova barra lateral. Não é mais necessárioa alternar entre espaços de trabalho. Categorias personalizadas em suas estações prévias de trabalho foram automaticamente criadas para você como parte do seu upgrade para v7.2. Estas podem ser removidas ou editadas de acordo com a sua preferência.", "SidebarTour.SidebarCategories.Link": "Saiba mais", "SidebarTour.SidebarCategories.Title": "Categorias de barra lateral", + "SiteStats.total_boards": "Total de boards", "TableComponent.add-icon": "Adicionar Ícone", "TableComponent.name": "Nome", "TableComponent.plus-new": "+ Novo", @@ -267,6 +281,7 @@ "TableHeaderMenu.insert-right": "Inserir à direita", "TableHeaderMenu.sort-ascending": "Ordem ascendente", "TableHeaderMenu.sort-descending": "Ordem descendente", + "TableRow.MoreOption": "Mais ações", "TableRow.open": "Abrir", "TopBar.give-feedback": "Dar feedback", "URLProperty.copiedLink": "Copiado!", @@ -348,6 +363,7 @@ "calendar.month": "Mês", "calendar.today": "HOJE", "calendar.week": "Semana", + "centerPanel.unknown-user": "Usuário desconhecido", "cloudMessage.learn-more": "Saiba mais", "createImageBlock.failed": "Não foi possível enviar o arquivo. Limite de tamanho alcançado.", "default-properties.badges": "Comentários e descrição", @@ -369,6 +385,7 @@ "login.log-in-button": "Entrar", "login.log-in-title": "Entrar", "login.register-button": "ou criar uma conta se você ainda não tiver uma", + "new_channel_modal.create_board.select_template_placeholder": "Selecionar um modelo", "notification-box-card-limit-reached.close-tooltip": "Soneca por 10 dias", "notification-box-card-limit-reached.contact-link": "notificar seu admin", "notification-box-card-limit-reached.link": "Atualizar para um plano pago", @@ -388,14 +405,14 @@ "rhs-boards.dm": "DM", "rhs-boards.gm": "GM", "rhs-boards.header.dm": "esta Direct Message", - "rhs-boards.header.gm": "Este Gruop Message", + "rhs-boards.header.gm": "esta mensagem de grupo", "rhs-boards.last-update-at": "Última atualização em: {datetime}", "rhs-boards.link-boards-to-channel": "Vincular boards para {channelName}", "rhs-boards.linked-boards": "Boards vinculados", "rhs-boards.no-boards-linked-to-channel": "Nenhum board está vinculado a {channelName} ainda", "rhs-boards.no-boards-linked-to-channel-description": "Boards é uma ferramenta de gerenciamento de projeto que ajuda a definir, organizar, rastrear e gerenciar o trabalho entre times, usando uma visualização de quadro estilo Kaban familiar.", "rhs-boards.unlink-board": "Desvincular board", - "rhs-boards.unlink-board1": "Desvincular board Hello", + "rhs-boards.unlink-board1": "Desvincular board", "rhs-channel-boards-header.title": "Boards", "share-board.publish": "Publicar", "share-board.share": "Compartilhar", From d16bbcbe10fba7d525c836c7c84d3cacbe4eb3c1 Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Mon, 6 Mar 2023 08:48:30 -0700 Subject: [PATCH 50/56] Fix table multiperson (#4613) * clear overflow on multiperson * clear overflow on multiperson --- webapp/src/components/table/table.scss | 1 + 1 file changed, 1 insertion(+) 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; From fe29ec882619d0fd0fd2a271c605061d478c1a55 Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Mon, 6 Mar 2023 12:26:39 -0700 Subject: [PATCH 51/56] check permissions to channel before patching via api --- server/app/boards.go | 13 +++++++++ server/app/boards_test.go | 61 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/server/app/boards.go b/server/app/boards.go index 2ddee918f..d31bd573c 100644 --- a/server/app/boards.go +++ b/server/app/boards.go @@ -355,12 +355,15 @@ func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*mode var oldMembers []*model.BoardMember if patch.Type != nil || patch.ChannelID != nil { + testChannel := "" if patch.ChannelID != nil && *patch.ChannelID == "" { var err error oldMembers, err = a.GetMembersForBoard(boardID) if err != nil { a.logger.Error("Unable to get the board members", mlog.Err(err)) } + } else if patch.ChannelID != nil && *patch.ChannelID != "" { + testChannel = *patch.ChannelID } board, err := a.store.GetBoard(boardID) @@ -372,7 +375,17 @@ func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*mode } oldChannelID = board.ChannelID isTemplate = board.IsTemplate + if testChannel == "" { + testChannel = oldChannelID + } + + if testChannel != "" { + if !a.permissions.HasPermissionToChannel(userID, testChannel, model.PermissionCreatePost) { + return nil, model.NewErrPermission("access denied to channel") + } + } } + updatedBoard, err := a.store.PatchBoard(boardID, patch, userID) if err != nil { return nil, err diff --git a/server/app/boards_test.go b/server/app/boards_test.go index fc9771e54..ee851fdb8 100644 --- a/server/app/boards_test.go +++ b/server/app/boards_test.go @@ -399,6 +399,67 @@ func TestPatchBoard(t *testing.T) { require.NoError(t, err) require.Equal(t, boardID, patchedBoard.ID) }) + + t.Run("patch type channel, user without post permissions", func(t *testing.T) { + const boardID = "board_id_1" + const userID = "user_id_2" + const teamID = "team_id_1" + + channelID := "myChannel" + patchType := model.BoardTypeOpen + patch := &model.BoardPatch{ + Type: &patchType, + ChannelID: &channelID, + } + + // Type not nil, will cause board to be reteived + // to check isTemplate + th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ + ID: boardID, + TeamID: teamID, + IsTemplate: true, + }, nil).Times(1) + + th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(false).Times(1) + _, err := th.App.PatchBoard(patch, boardID, userID) + require.Error(t, err) + }) + + t.Run("patch type remove channel, user without post permissions", func(t *testing.T) { + const boardID = "board_id_1" + const userID = "user_id_2" + const teamID = "team_id_1" + + channelID := "myChannel" + clearChannel := "" + patchType := model.BoardTypeOpen + patch := &model.BoardPatch{ + Type: &patchType, + ChannelID: &clearChannel, + } + + // Type not nil, will cause board to be reteived + // to check isTemplate + th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ + ID: boardID, + TeamID: teamID, + IsTemplate: true, + ChannelID: channelID, + }, nil).Times(2) + + th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(false).Times(1) + + th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) + // Should call GetMembersForBoard 2 times + // for WS BroadcastBoardChange + // for AddTeamMembers check + // We are returning the user as a direct Board Member, so BroadcastMemberDelete won't be called + th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{{BoardID: boardID, UserID: userID, SchemeEditor: true}}, nil).Times(1) + + _, err := th.App.PatchBoard(patch, boardID, userID) + require.Error(t, err) + }) + } func TestGetBoardCount(t *testing.T) { From 5ac5eadbeeecbfbcfe7db3e74726585f73b00d99 Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Mon, 6 Mar 2023 15:10:58 -0700 Subject: [PATCH 52/56] adding a successful test --- server/app/boards_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/server/app/boards_test.go b/server/app/boards_test.go index ee851fdb8..7830c9297 100644 --- a/server/app/boards_test.go +++ b/server/app/boards_test.go @@ -185,6 +185,7 @@ func TestPatchBoard(t *testing.T) { // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil) + th.Store.EXPECT().GetUserByID(userID).Return(&model.User{ID: userID, Username: "UserName"}, nil) th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( &model.Board{ @@ -425,6 +426,44 @@ func TestPatchBoard(t *testing.T) { require.Error(t, err) }) + t.Run("patch type channel, user with post permissions", func(t *testing.T) { + const boardID = "board_id_1" + const userID = "user_id_2" + const teamID = "team_id_1" + + channelID := "myChannel" + patch := &model.BoardPatch{ + ChannelID: &channelID, + } + + // Type not nil, will cause board to be reteived + // to check isTemplate + th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ + ID: boardID, + TeamID: teamID, + }, nil).Times(2) + + th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(true).Times(1) + + th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( + &model.Board{ + ID: boardID, + TeamID: teamID, + }, + nil) + + // Should call GetMembersForBoard 2 times + // - for WS BroadcastBoardChange + // - for AddTeamMembers check + th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(2) + + th.Store.EXPECT().PostMessage(utils.Anything, "", "").Times(1) + + patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) + require.NoError(t, err) + require.Equal(t, boardID, patchedBoard.ID) + }) + t.Run("patch type remove channel, user without post permissions", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_2" From fa72286427d0d11bc10638625ad581795cfbc46f Mon Sep 17 00:00:00 2001 From: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Date: Tue, 7 Mar 2023 10:21:53 +0530 Subject: [PATCH 53/56] Board teamless file path (#4577) * Updated upload and donwload code * Removed archived file handling as we no longer have cloud limits * Fixed server lint * Restored unused * CI * Added new tests * Fixed integration tests * Added Path column for personal server's FileInfo table * Removed sophesticated, elegant debug logs --------- Co-authored-by: Mattermost Build --- server/api/api.go | 2 +- server/api/files.go | 27 +----- server/app/files.go | 41 ++++++++- server/app/files_test.go | 90 +++++++++++++++++-- server/client/client.go | 9 ++ server/services/store/sqlstore/file.go | 4 + .../000039_add_path_to_file_info.down.sql | 1 + .../000039_add_path_to_file_info.up.sql | 1 + server/utils/utils.go | 5 ++ 9 files changed, 145 insertions(+), 35 deletions(-) create mode 100644 server/services/store/sqlstore/migrations/000039_add_path_to_file_info.down.sql create mode 100644 server/services/store/sqlstore/migrations/000039_add_path_to_file_info.up.sql 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")) +} From dda3b8f13a521edbdfef6bc401db1762adccc223 Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Tue, 7 Mar 2023 11:01:52 -0700 Subject: [PATCH 54/56] lint fixes --- server/app/boards_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/app/boards_test.go b/server/app/boards_test.go index 7830c9297..8cd3679f2 100644 --- a/server/app/boards_test.go +++ b/server/app/boards_test.go @@ -469,7 +469,7 @@ func TestPatchBoard(t *testing.T) { const userID = "user_id_2" const teamID = "team_id_1" - channelID := "myChannel" + const channelID = "myChannel" clearChannel := "" patchType := model.BoardTypeOpen patch := &model.BoardPatch{ @@ -498,7 +498,6 @@ func TestPatchBoard(t *testing.T) { _, err := th.App.PatchBoard(patch, boardID, userID) require.Error(t, err) }) - } func TestGetBoardCount(t *testing.T) { From e59b61937f8197bea16e7754205a130ebdcda83e Mon Sep 17 00:00:00 2001 From: Harshil Sharma Date: Thu, 9 Mar 2023 11:20:32 +0530 Subject: [PATCH 55/56] Counting on specific column --- server/services/store/sqlstore/data_migrations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/services/store/sqlstore/data_migrations.go b/server/services/store/sqlstore/data_migrations.go index 15d86129d..bbc7e2218 100644 --- a/server/services/store/sqlstore/data_migrations.go +++ b/server/services/store/sqlstore/data_migrations.go @@ -853,7 +853,7 @@ func (s *SQLStore) doesDuplicateCategoryBoardsExist() (bool, error) { Having("count(*) > 1") query := s.getQueryBuilder(s.db). - Select("COUNT(*)"). + Select("COUNT(user_id)"). FromSelect(subQuery, "duplicate_dataset") row := query.QueryRow() From 41fbd5fecc2e05152dfeb590676ce7d97e1c4510 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 9 Mar 2023 17:53:45 +0100 Subject: [PATCH 56/56] Enable cross-compilation of Linux webapp via Docker (#4528) Relates to: #4470 Co-authored-by: Mattermost Build --- Dockerfile.build | 36 +++++++++++++++++++----------------- Makefile | 4 ++-- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Dockerfile.build b/Dockerfile.build index 6cf014765..922c28c1d 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -1,10 +1,10 @@ -# This dockerfile is used to build Focalboard for Linux -# it builds all the parts inside the container and the last stage just holds the -# package that can be extracted using docker cp command -# ie -# docker build -f Dockerfile.build --no-cache -t focalboard-build:dirty . -# docker run --rm -v /tmp/dist:/tmp -d --name test focalboard-build:dirty /bin/sh -c 'sleep 1000' -# docker cp test:/dist/focalboard-server-linux-amd64.tar.gz . +# This Dockerfile is used to build Focalboard for Linux. It builds all the parts inside the image +# and the last stage just holds the package which is then copied back to the host. +# +# docker buildx build -f Dockerfile.build --no-cache --platform linux/amd64 -t focalboard-build:dirty --output out . +# docker buildx build -f Dockerfile.build --no-cache --platform linux/arm64 -t focalboard-build:dirty --output out . +# +# Afterwards the packages can be found in the ./out folder. # build frontend FROM node:16.3.0@sha256:ca6daf1543242acb0ca59ff425509eab7defb9452f6ae07c156893db06c7a9a4 AS frontend @@ -12,8 +12,10 @@ FROM node:16.3.0@sha256:ca6daf1543242acb0ca59ff425509eab7defb9452f6ae07c156893db WORKDIR /webapp COPY webapp . -RUN npm install --no-optional -RUN npm run pack +### 'CPPFLAGS="-DPNG_ARM_NEON_OPT=0"' Needed To Avoid Bug Described in: https://github.com/imagemin/optipng-bin/issues/118#issuecomment-1019838562 +### Can be Removed when Ticket will be Closed +RUN CPPFLAGS="-DPNG_ARM_NEON_OPT=0" npm install --no-optional && \ + npm run pack # build backend and package FROM golang:1.18.3@sha256:b203dc573d81da7b3176264bfa447bd7c10c9347689be40540381838d75eebef AS backend @@ -21,13 +23,13 @@ FROM golang:1.18.3@sha256:b203dc573d81da7b3176264bfa447bd7c10c9347689be405403818 COPY . . COPY --from=frontend /webapp/pack webapp/pack +ARG TARGETARCH + # RUN apt-get update && apt-get install libgtk-3-dev libwebkit2gtk-4.0-dev -y -RUN make server-linux -RUN make server-linux-package-docker +RUN EXCLUDE_PLUGIN=true EXCLUDE_SERVER=true EXCLUDE_ENTERPRISE=true make server-linux arch=${TARGETARCH} +RUN make server-linux-package-docker arch=${TARGETARCH} -# just hold the packages to output later -FROM alpine:3.12@sha256:c75ac27b49326926b803b9ed43bf088bc220d22556de1bc5f72d742c91398f69 AS dist - -WORKDIR /dist - -COPY --from=backend /go/dist/focalboard-server-linux-amd64.tar.gz . +# Copy package back to host +FROM scratch AS dist +ARG TARGETARCH +COPY --from=backend /go/dist/focalboard-server-linux-${TARGETARCH}.tar.gz . diff --git a/Makefile b/Makefile index b6e13291c..9d74bef1f 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ endif server-linux: setup-go-work ## Build server for Linux. mkdir -p bin/linux $(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=linux") - cd server; env GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/linux/focalboard-server ./main + cd server; env GOOS=linux GOARCH=$(arch) go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/linux/focalboard-server ./main server-docker: setup-go-work ## Build server for Docker Architectures. mkdir -p bin/docker @@ -101,7 +101,7 @@ server-linux-package-docker: cp NOTICE.txt package/${PACKAGE_FOLDER} cp webapp/NOTICE.txt package/${PACKAGE_FOLDER}/webapp-NOTICE.txt mkdir -p dist - cd package && tar -czvf ../dist/focalboard-server-linux-amd64.tar.gz ${PACKAGE_FOLDER} + cd package && tar -czvf ../dist/focalboard-server-linux-$(arch).tar.gz ${PACKAGE_FOLDER} rm -rf package generate: ## Install and run code generators.