1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-12-24 13:43:12 +02:00

Merge branch 'main' into compliance-history-export

This commit is contained in:
Mattermost Build 2023-01-21 04:59:49 +02:00 committed by GitHub
commit a422f213d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 253 additions and 60 deletions

View File

@ -49,6 +49,11 @@ func (a *serviceAPIAdapter) GetDirectChannel(userID1, userID2 string) (*mm_model
return channel, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) {
channel, appErr := a.api.channelService.GetDirectChannelOrCreate(userID1, userID2)
return channel, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetChannelByID(channelID string) (*mm_model.Channel, error) {
channel, appErr := a.api.channelService.GetChannelByID(channelID)
return channel, normalizeAppErr(appErr)

View File

@ -54,6 +54,12 @@ func (a *pluginAPIAdapter) GetDirectChannel(userID1, userID2 string) (*mm_model.
return channel, normalizeAppErr(appErr)
}
func (a *pluginAPIAdapter) GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) {
// plugin API's GetDirectChannel will create channel if it does not exist.
channel, appErr := a.api.GetDirectChannel(userID1, userID2)
return channel, normalizeAppErr(appErr)
}
func (a *pluginAPIAdapter) GetChannelByID(channelID string) (*mm_model.Channel, error) {
channel, appErr := a.api.GetChannel(channelID)
return channel, normalizeAppErr(appErr)

View File

@ -80,6 +80,9 @@ func (b *BoardsApp) OnConfigurationChange() error {
if mmconfig.PluginSettings.Plugins[PluginName][SharedBoardsName] == true {
enableShareBoards = true
}
if mmconfig.ProductSettings.EnablePublicSharedBoards != nil {
enableShareBoards = *mmconfig.ProductSettings.EnablePublicSharedBoards
}
configuration := &configuration{
EnablePublicSharedBoards: enableShareBoards,
}

View File

@ -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
}
]
}

View File

@ -92,14 +92,25 @@ func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.Exp
return err
}
if block.Type == model.TypeImage {
filename, err := extractImageFilename(block)
if err != nil {
filename, err2 := extractImageFilename(block)
if err2 != nil {
return err
}
files = append(files, filename)
}
}
boardMembers, err := a.GetMembersForBoard(board.ID)
if err != nil {
return err
}
for _, boardMember := range boardMembers {
if err = a.writeArchiveBoardMemberLine(w, boardMember); err != nil {
return err
}
}
// write the files
for _, filename := range files {
if err := a.writeArchiveFile(zw, filename, board.ID, opt); err != nil {
@ -109,6 +120,31 @@ func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.Exp
return nil
}
// writeArchiveBoardMemberLine writes a single boardMember to the archive.
func (a *App) writeArchiveBoardMemberLine(w io.Writer, boardMember *model.BoardMember) error {
bm, err := json.Marshal(&boardMember)
if err != nil {
return err
}
line := model.ArchiveLine{
Type: "boardMember",
Data: bm,
}
bm, err = json.Marshal(&line)
if err != nil {
return err
}
_, err = w.Write(bm)
if err != nil {
return err
}
_, err = w.Write(newline)
return err
}
// writeArchiveBlockLine writes a single block to the archive.
func (a *App) writeArchiveBlockLine(w io.Writer, block *model.Block) error {
b, err := json.Marshal(&block)

View File

@ -137,6 +137,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
}
now := utils.GetMillis()
var boardID string
var boardMembers []*model.BoardMember
lineNum := 1
firstLine := true
@ -196,6 +197,12 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
block.UpdateAt = now
block.BoardID = boardID
boardsAndBlocks.Blocks = append(boardsAndBlocks.Blocks, block)
case "boardMember":
var boardMember *model.BoardMember
if err2 := json.Unmarshal(archiveLine.Data, &boardMember); err2 != nil {
return "", fmt.Errorf("invalid board Member in archive line %d: %w", lineNum, err2)
}
boardMembers = append(boardMembers, boardMember)
default:
return "", model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type)
}
@ -212,6 +219,13 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
lineNum++
}
// loop to remove the people how are not part of the team and system
for i := len(boardMembers) - 1; i >= 0; i-- {
if _, err := a.GetUser(boardMembers[i].UserID); err != nil {
boardMembers = append(boardMembers[:i], boardMembers[i+1:]...)
}
}
a.fixBoardsandBlocks(boardsAndBlocks, opt)
var err error
@ -225,16 +239,22 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
return "", fmt.Errorf("error inserting archive blocks: %w", err)
}
// add user to all the new boards (if not the fake system user).
if opt.ModifiedBy != model.SystemUserID {
for _, board := range boardsAndBlocks.Boards {
boardMember := &model.BoardMember{
BoardID: board.ID,
UserID: opt.ModifiedBy,
SchemeAdmin: true,
// add users to all the new boards (if not the fake system user).
for _, board := range boardsAndBlocks.Boards {
for _, boardMember := range boardMembers {
bm := &model.BoardMember{
BoardID: board.ID,
UserID: boardMember.UserID,
Roles: boardMember.Roles,
MinimumRole: boardMember.MinimumRole,
SchemeAdmin: boardMember.SchemeAdmin,
SchemeEditor: boardMember.SchemeEditor,
SchemeCommenter: boardMember.SchemeCommenter,
SchemeViewer: boardMember.SchemeViewer,
Synthetic: boardMember.Synthetic,
}
if _, err := a.AddMemberToBoard(boardMember); err != nil {
return "", fmt.Errorf("cannot add member to board: %w", err)
if _, err2 := a.AddMemberToBoard(bm); err2 != nil {
return "", fmt.Errorf("cannot add member to board: %w", err2)
}
}
}

View File

@ -47,8 +47,6 @@ func TestApp_ImportArchive(t *testing.T) {
th.Store.EXPECT().CreateBoardsAndBlocks(gomock.AssignableToTypeOf(&model.BoardsAndBlocks{}), "user").Return(babs, nil)
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{boardMember}, nil)
th.Store.EXPECT().GetBoard(board.ID).Return(board, nil)
th.Store.EXPECT().GetMemberForBoard(board.ID, "user").Return(boardMember, nil)
th.Store.EXPECT().GetUserCategoryBoards("user", "test-team")
th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil)
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
@ -62,6 +60,64 @@ func TestApp_ImportArchive(t *testing.T) {
err := th.App.ImportArchive(r, opts)
require.NoError(t, err, "import archive should not fail")
})
t.Run("import board archive", func(t *testing.T) {
r := bytes.NewReader([]byte(boardArchive))
opts := model.ImportArchiveOptions{
TeamID: "test-team",
ModifiedBy: "f1tydgc697fcbp8ampr6881jea",
}
bm1 := &model.BoardMember{
BoardID: board.ID,
UserID: "f1tydgc697fcbp8ampr6881jea",
}
bm2 := &model.BoardMember{
BoardID: board.ID,
UserID: "hxxzooc3ff8cubsgtcmpn8733e",
}
bm3 := &model.BoardMember{
BoardID: board.ID,
UserID: "nto73edn5ir6ifimo5a53y1dwa",
}
user1 := &model.User{
ID: "f1tydgc697fcbp8ampr6881jea",
}
user2 := &model.User{
ID: "hxxzooc3ff8cubsgtcmpn8733e",
}
user3 := &model.User{
ID: "nto73edn5ir6ifimo5a53y1dwa",
}
th.Store.EXPECT().CreateBoardsAndBlocks(gomock.AssignableToTypeOf(&model.BoardsAndBlocks{}), "f1tydgc697fcbp8ampr6881jea").Return(babs, nil)
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{bm1, bm2, bm3}, nil)
th.Store.EXPECT().GetUserCategoryBoards("f1tydgc697fcbp8ampr6881jea", "test-team")
th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil)
th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{
ID: "boards_category_id",
Name: "Boards",
}, nil)
th.Store.EXPECT().GetMembersForUser("f1tydgc697fcbp8ampr6881jea").Return([]*model.BoardMember{}, nil)
th.Store.EXPECT().GetBoardsForUserAndTeam("f1tydgc697fcbp8ampr6881jea", "test-team", false).Return([]*model.Board{}, nil)
th.Store.EXPECT().AddUpdateCategoryBoard("f1tydgc697fcbp8ampr6881jea", utils.Anything).Return(nil)
th.Store.EXPECT().GetBoard(board.ID).AnyTimes().Return(board, nil)
th.Store.EXPECT().GetMemberForBoard(board.ID, "f1tydgc697fcbp8ampr6881jea").AnyTimes().Return(bm1, nil)
th.Store.EXPECT().GetMemberForBoard(board.ID, "hxxzooc3ff8cubsgtcmpn8733e").AnyTimes().Return(bm2, nil)
th.Store.EXPECT().GetMemberForBoard(board.ID, "nto73edn5ir6ifimo5a53y1dwa").AnyTimes().Return(bm3, nil)
th.Store.EXPECT().GetUserByID("f1tydgc697fcbp8ampr6881jea").AnyTimes().Return(user1, nil)
th.Store.EXPECT().GetUserByID("hxxzooc3ff8cubsgtcmpn8733e").AnyTimes().Return(user2, nil)
th.Store.EXPECT().GetUserByID("nto73edn5ir6ifimo5a53y1dwa").AnyTimes().Return(user3, nil)
boardID, err := th.App.ImportBoardJSONL(r, opts)
require.Equal(t, board.ID, boardID, "Board ID should be same")
require.NoError(t, err, "import archive should not fail")
})
}
//nolint:lll
@ -78,3 +134,12 @@ const asana = `{"version":1,"date":1614714686842}
{"type":"block","data":{"id":"db1dd596-0999-4741-8b05-72ca8e438e31","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"deaab476-c690-48df-828f-725b064dc476"},"contentOrder":[]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[EXAMPLE TASK] Approve campaign copy"}}
{"type":"block","data":{"id":"16861c05-f31f-46af-8429-80a87b5aa93a","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"2138305a-3157-461c-8bbe-f19ebb55846d"},"contentOrder":[]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[EXAMPLE TASK] Send out updated attendee list"}}
`
//nolint:lll
const boardArchive = `{"type":"board","data":{"id":"bfoi6yy6pa3yzika53spj7pq9ee","teamId":"wsmqbtwb5jb35jb3mtp85c8a9h","channelId":"","createdBy":"nto73edn5ir6ifimo5a53y1dwa","modifiedBy":"nto73edn5ir6ifimo5a53y1dwa","type":"P","minimumRole":"","title":"Custom","description":"","icon":"","showDescription":false,"isTemplate":false,"templateVersion":0,"properties":{},"cardProperties":[{"id":"aonihehbifijmx56aqzu3cc7w1r","name":"Status","options":[],"type":"select"},{"id":"aohjkzt769rxhtcz1o9xcoce5to","name":"Person","options":[],"type":"person"}],"createAt":1672750481591,"updateAt":1672750481591,"deleteAt":0}}
{"type":"block","data":{"id":"ckpc3b1dp3pbw7bqntfryy9jbzo","parentId":"bjaqxtbyqz3bu7pgyddpgpms74a","createdBy":"nto73edn5ir6ifimo5a53y1dwa","modifiedBy":"nto73edn5ir6ifimo5a53y1dwa","schema":1,"type":"card","title":"Test","fields":{"contentOrder":[],"icon":"","isTemplate":false,"properties":{"aohjkzt769rxhtcz1o9xcoce5to":"hxxzooc3ff8cubsgtcmpn8733e"}},"createAt":1672750481612,"updateAt":1672845003530,"deleteAt":0,"boardId":"bfoi6yy6pa3yzika53spj7pq9ee"}}
{"type":"block","data":{"id":"v7tdajwpm47r3u8duedk89bhxar","parentId":"bpypang3a3errqstj1agx9kuqay","createdBy":"nto73edn5ir6ifimo5a53y1dwa","modifiedBy":"nto73edn5ir6ifimo5a53y1dwa","schema":1,"type":"view","title":"Board view","fields":{"cardOrder":["crsyw7tbr3pnjznok6ppngmmyya","c5titiemp4pgaxbs4jksgybbj4y"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":[],"visiblePropertyIds":["aohjkzt769rxhtcz1o9xcoce5to"]},"createAt":1672750481626,"updateAt":1672750481626,"deleteAt":0,"boardId":"bfoi6yy6pa3yzika53spj7pq9ee"}}
{"type":"boardMember","data":{"boardId":"bfoi6yy6pa3yzika53spj7pq9ee","userId":"f1tydgc697fcbp8ampr6881jea","roles":"","minimumRole":"","schemeAdmin":false,"schemeEditor":false,"schemeCommenter":false,"schemeViewer":true,"synthetic":false}}
{"type":"boardMember","data":{"boardId":"bfoi6yy6pa3yzika53spj7pq9ee","userId":"hxxzooc3ff8cubsgtcmpn8733e","roles":"","minimumRole":"","schemeAdmin":false,"schemeEditor":false,"schemeCommenter":false,"schemeViewer":true,"synthetic":false}}
{"type":"boardMember","data":{"boardId":"bfoi6yy6pa3yzika53spj7pq9ee","userId":"nto73edn5ir6ifimo5a53y1dwa","roles":"","minimumRole":"","schemeAdmin":true,"schemeEditor":false,"schemeCommenter":false,"schemeViewer":false,"synthetic":false}}
`

View File

@ -199,6 +199,21 @@ func (mr *MockServicesAPIMockRecorder) GetDirectChannel(arg0, arg1 interface{})
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDirectChannel", reflect.TypeOf((*MockServicesAPI)(nil).GetDirectChannel), arg0, arg1)
}
// GetDirectChannelOrCreate mocks base method.
func (m *MockServicesAPI) GetDirectChannelOrCreate(arg0, arg1 string) (*model.Channel, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDirectChannelOrCreate", arg0, arg1)
ret0, _ := ret[0].(*model.Channel)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDirectChannelOrCreate indicates an expected call of GetDirectChannelOrCreate.
func (mr *MockServicesAPIMockRecorder) GetDirectChannelOrCreate(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDirectChannelOrCreate", reflect.TypeOf((*MockServicesAPI)(nil).GetDirectChannelOrCreate), arg0, arg1)
}
// GetFileInfo mocks base method.
func (m *MockServicesAPI) GetFileInfo(arg0 string) (*model.FileInfo, error) {
m.ctrl.T.Helper()

View File

@ -30,6 +30,7 @@ var FocalboardBot = &mm_model.Bot{
type ServicesAPI interface {
// Channels service
GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error)
GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error)
GetChannelByID(channelID string) (*mm_model.Channel, error)
GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error)
GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error)

View File

@ -8,9 +8,9 @@ import (
)
type servicesAPI interface {
// GetDirectChannel gets a direct message channel.
// If the channel does not exist it will create it.
GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error)
// GetDirectChannelOrCreate gets a direct message channel,
// or creates one if it does not already exist
GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error)
// CreatePost creates a post.
CreatePost(post *mm_model.Post) (*mm_model.Post, error)

View File

@ -53,7 +53,7 @@ func (pd *PluginDelivery) getDirectChannelID(teamID string, subscriberID string,
return "", fmt.Errorf("cannot find user: %w", err)
}
channel, err := pd.getDirectChannel(teamID, user.Id, botID)
if err != nil {
if err != nil || channel == nil {
return "", fmt.Errorf("cannot get direct channel: %w", err)
}
return channel.Id, nil
@ -70,5 +70,5 @@ func (pd *PluginDelivery) getDirectChannel(teamID string, userID string, botID s
if err != nil {
return nil, fmt.Errorf("cannot add bot to team %s: %w", teamID, err)
}
return pd.api.GetDirectChannel(userID, botID)
return pd.api.GetDirectChannelOrCreate(userID, botID)
}

View File

@ -101,6 +101,10 @@ func (m servicesAPIMock) GetDirectChannel(userID1, userID2 string) (*mm_model.Ch
return nil, nil
}
func (m servicesAPIMock) GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) {
return nil, nil
}
func (m servicesAPIMock) CreatePost(post *mm_model.Post) (*mm_model.Post, error) {
return post, nil
}

View File

@ -28,7 +28,7 @@ describe('Create and delete board / card', () => {
cy.contains('Project Tasks').should('exist')
// Create empty board
cy.contains('Create empty board').should('exist').click({force: true})
cy.contains('Create an empty board').should('exist').click({force: true})
cy.get('.BoardComponent').should('exist')
cy.get('.Editable.title').invoke('attr', 'placeholder').should('contain', 'Untitled board')

View File

@ -17,5 +17,5 @@ Cypress.Commands.add('uiCreateEmptyBoard', () => {
cy.log('Create new empty board')
cy.contains('+ Add board').should('be.visible').click().wait(500)
return cy.contains('Create empty board').click({force: true}).wait(1000)
return cy.contains('Create an empty board').click({force: true}).wait(1000)
})

View File

@ -1,12 +1,12 @@
{
"AppBar.Tooltip": "Toggle Linked Boards",
"AppBar.Tooltip": "Toggle linked boards",
"Attachment.Attachment-title": "Attachment",
"AttachmentBlock.DeleteAction": "delete",
"AttachmentBlock.addElement": "add {type}",
"AttachmentBlock.delete": "Attachment Deleted Successfully.",
"AttachmentBlock.failed": "Unable to upload the file. Attachment size limit reached.",
"AttachmentBlock.delete": "Attachment deleted.",
"AttachmentBlock.failed": "This file couldn't be uploaded as the file size limit has been reached.",
"AttachmentBlock.upload": "Attachment uploading.",
"AttachmentBlock.uploadSuccess": "Attachment uploaded successfull.",
"AttachmentBlock.uploadSuccess": "Attachment uploaded.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Delete",
"AttachmentElement.download": "Download",
"AttachmentElement.upload-percentage": "Uploading...({uploadPercent}%)",
@ -16,7 +16,7 @@
"BoardComponent.hide": "Hide",
"BoardComponent.new": "+ New",
"BoardComponent.no-property": "No {property}",
"BoardComponent.no-property-title": "Items with an empty {property} property will go here. This column cannot be removed.",
"BoardComponent.no-property-title": "Items with an empty {property} property will go here. This column can't be removed.",
"BoardComponent.show": "Show",
"BoardMember.schemeAdmin": "Admin",
"BoardMember.schemeCommenter": "Commenter",
@ -27,7 +27,7 @@
"BoardPage.newVersion": "A new version of Boards is available, click here to reload.",
"BoardPage.syncFailed": "Board may be deleted or access revoked.",
"BoardTemplateSelector.add-template": "Create new template",
"BoardTemplateSelector.create-empty-board": "Create empty board",
"BoardTemplateSelector.create-empty-board": "Create an empty board",
"BoardTemplateSelector.delete-template": "Delete",
"BoardTemplateSelector.description": "Add a board to the sidebar using any of the templates defined below or start from scratch.",
"BoardTemplateSelector.edit-template": "Edit",
@ -35,7 +35,7 @@
"BoardTemplateSelector.plugin.no-content-title": "Create a board",
"BoardTemplateSelector.title": "Create a board",
"BoardTemplateSelector.use-this-template": "Use this template",
"BoardsSwitcher.Title": "Find Boards",
"BoardsSwitcher.Title": "Find boards",
"BoardsUnfurl.Limited": "Additional details are hidden due to the card being archived",
"BoardsUnfurl.Remainder": "+{remainder} more",
"BoardsUnfurl.Updated": "Updated {time}",
@ -88,7 +88,7 @@
"CardDetail.add-icon": "Add icon",
"CardDetail.add-property": "+ Add a property",
"CardDetail.addCardText": "add card text",
"CardDetail.limited-body": "Upgrade to our Professional or Enterprise plan to view archived cards, have unlimited views per boards, unlimited cards and more.",
"CardDetail.limited-body": "Upgrade to our Professional or Enterprise plan.",
"CardDetail.limited-button": "Upgrade",
"CardDetail.limited-title": "This card is hidden",
"CardDetail.moveContent": "Move card content",
@ -103,9 +103,9 @@
"CardDetailProperty.property-deleted": "Deleted {propertyName} successfully!",
"CardDetailProperty.property-name-change-subtext": "type from \"{oldPropType}\" to \"{newPropType}\"",
"CardDetial.limited-link": "Learn more about our plans.",
"CardDialog.delete-confirmation-dialog-attachment": "Confirm Attachment delete!",
"CardDialog.delete-confirmation-dialog-attachment": "Confirm attachment delete",
"CardDialog.delete-confirmation-dialog-button-text": "Delete",
"CardDialog.delete-confirmation-dialog-heading": "Confirm card delete!",
"CardDialog.delete-confirmation-dialog-heading": "Confirm card delete",
"CardDialog.editing-template": "You're editing a template.",
"CardDialog.nocard": "This card doesn't exist or is inaccessible.",
"Categories.CreateCategoryDialog.CancelText": "Cancel",
@ -187,7 +187,7 @@
"OnboardingTour.AddComments.Title": "Add comments",
"OnboardingTour.AddDescription.Body": "Add a description to your card so your teammates know what the card is about.",
"OnboardingTour.AddDescription.Title": "Add description",
"OnboardingTour.AddProperties.Body": "Add various properties to cards to make them more powerful!",
"OnboardingTour.AddProperties.Body": "Add various properties to cards to make them more powerful.",
"OnboardingTour.AddProperties.Title": "Add properties",
"OnboardingTour.AddView.Body": "Go here to create a new view to organise your board using different layouts.",
"OnboardingTour.AddView.Title": "Add a new view",
@ -237,11 +237,11 @@
"ShareBoard.copyLink": "Copy link",
"ShareBoard.regenerate": "Regenerate token",
"ShareBoard.searchPlaceholder": "Search for people and channels",
"ShareBoard.teamPermissionsText": "Everyone at {teamName} Team",
"ShareBoard.teamPermissionsText": "Everyone at {teamName} team",
"ShareBoard.tokenRegenrated": "Token regenerated",
"ShareBoard.userPermissionsRemoveMemberText": "Remove member",
"ShareBoard.userPermissionsYouText": "(You)",
"ShareTemplate.Title": "Share Template",
"ShareTemplate.Title": "Share template",
"ShareTemplate.searchPlaceholder": "Search for people",
"Sidebar.about": "About Focalboard",
"Sidebar.add-board": "+ Add board",
@ -277,8 +277,8 @@
"SidebarTour.SidebarCategories.Body": "All your boards are now organized under your new sidebar. No more switching between workspaces. One-time custom categories based on your prior workspaces may have automatically been created for you as part of your v7.2 upgrade. These can be removed or edited to your preference.",
"SidebarTour.SidebarCategories.Link": "Learn more",
"SidebarTour.SidebarCategories.Title": "Sidebar categories",
"SiteStats.total_boards": "Total Boards",
"SiteStats.total_cards": "Total Cards",
"SiteStats.total_boards": "Total boards",
"SiteStats.total_cards": "Total cards",
"TableComponent.add-icon": "Add icon",
"TableComponent.name": "Name",
"TableComponent.plus-new": "+ New",
@ -342,9 +342,9 @@
"ViewLimitDialog.Heading": "Views per board limit reached",
"ViewLimitDialog.PrimaryButton.Title.Admin": "Upgrade",
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "Notify Admin",
"ViewLimitDialog.Subtext.Admin": "Upgrade to our Professional or Enterprise plan to have unlimited views per boards, unlimited cards, and more.",
"ViewLimitDialog.Subtext.Admin": "Upgrade to our Professional or Enterprise plan.",
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "Learn more about our plans.",
"ViewLimitDialog.Subtext.RegularUser": "Notify your Admin to upgrade to our Professional or Enterprise plan to have unlimited views per boards, unlimited cards, and more.",
"ViewLimitDialog.Subtext.RegularUser": "Notify your Admin to upgrade to our Professional or Enterprise plan.",
"ViewLimitDialog.UpgradeImg.AltText": "upgrade image",
"ViewLimitDialog.notifyAdmin.Success": "Your admin has been notified",
"ViewTitle.hide-description": "hide description",
@ -375,10 +375,10 @@
"centerPanel.undefined": "No {propertyName}",
"centerPanel.unknown-user": "Unknown user",
"cloudMessage.learn-more": "Learn more",
"createImageBlock.failed": "Unable to upload the file. File size limit reached.",
"createImageBlock.failed": "This file couldn't be uploaded as the file size limit has been reached.",
"default-properties.badges": "Comments and description",
"default-properties.title": "Title",
"error.back-to-home": "Back to Home",
"error.back-to-home": "Back to home",
"error.back-to-team": "Back to team",
"error.board-not-found": "Board not found.",
"error.go-login": "Log in",
@ -390,7 +390,7 @@
"generic.previous": "Previous",
"guest-no-board.subtitle": "You don't have access to any board in this team yet, please wait until somebody adds you to any board.",
"guest-no-board.title": "No boards yet",
"imagePaste.upload-failed": "Some files not uploaded. File size limit reached",
"imagePaste.upload-failed": "Some files weren't uploaded because the file size limit has been reached.",
"limitedCard.title": "Cards hidden",
"login.log-in-button": "Log in",
"login.log-in-title": "Log in",
@ -406,15 +406,15 @@
"person.add-user-to-board-confirm-button": "Add to board",
"person.add-user-to-board-permissions": "Permissions",
"person.add-user-to-board-question": "Do you want to add {username} to the board?",
"person.add-user-to-board-warning": "{username} is not a member of the board, and will not receive any notifications about it.",
"person.add-user-to-board-warning": "{username} isn't a member of the board, and won't receive any notifications about it.",
"register.login-button": "or log in if you already have an account",
"register.signup-title": "Sign up for your account",
"rhs-board-non-admin-msg": "You are not an admin of the board",
"rhs-board-non-admin-msg": "You're not an admin of the board",
"rhs-boards.add": "Add",
"rhs-boards.dm": "DM",
"rhs-boards.gm": "GM",
"rhs-boards.header.dm": "this Direct Message",
"rhs-boards.header.gm": "this Group Message",
"rhs-boards.header.dm": "this direct message",
"rhs-boards.header.gm": "this group message",
"rhs-boards.last-update-at": "Last update at: {datetime}",
"rhs-boards.link-boards-to-channel": "Link boards to {channelName}",
"rhs-boards.linked-boards": "Linked boards",
@ -445,4 +445,4 @@
"tutorial_tip.ok": "Next",
"tutorial_tip.out": "Opt out of these tips.",
"tutorial_tip.seen": "Seen this before?"
}
}

View File

@ -165,7 +165,7 @@
"FilterByText.placeholder": "過濾文字",
"FilterComponent.add-filter": "+ 增加過濾條件",
"FilterComponent.delete": "刪除",
"FindBoardsDialog.IntroText": "查詢板",
"FindBoardsDialog.IntroText": "查詢板",
"FindBoardsDialog.NoResultsFor": "「{searchQuery}」搜尋未果",
"FindBoardsDialog.NoResultsSubtext": "檢查錯字或嘗試其他搜尋.",
"FindBoardsDialog.SubTitle": "輸入已找到面板.使用 <b>UP/DOWN</b>來瀏覽.<b>ENTER</b>來搜尋, <b>ESC</b> 來取消",
@ -313,8 +313,9 @@
"View.NewTemplateDefaultTitle": "沒有標題的模板",
"View.NewTemplateTitle": "沒有標題",
"View.Table": "圖表",
"ViewHeader.add-template": "+ 新範本",
"ViewHeader.add-template": "新範本",
"ViewHeader.delete-template": "刪除",
"ViewHeader.display-by": "依據{property}顯示",
"ViewHeader.edit-template": "編輯",
"ViewHeader.empty-card": "清空卡片",
"ViewHeader.export-board-archive": "匯出版面打包檔",
@ -331,11 +332,14 @@
"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.Admin.PricingPageLink": "了解更多我們的計畫。",
"ViewLimitDialog.Subtext.RegularUser": "通知你的管理員升級到專業版或是企業版,獲得無限使用看板、卡片、更多。",
"ViewLimitDialog.UpgradeImg.AltText": "升級圖片",
"ViewLimitDialog.notifyAdmin.Success": "已經通知管理者",
"ViewTitle.hide-description": "隱藏敘述",
@ -344,14 +348,17 @@
"ViewTitle.remove-icon": "移除圖示",
"ViewTitle.show-description": "顯示敘述",
"ViewTitle.untitled-board": "未命名版面",
"WelcomePage.Description": "看板是一個專案管理工具,可以使用熟悉的圖表幫助我們定義、組織、追蹤和管理跨團隊工作。",
"WelcomePage.Explore.Button": "探索",
"WelcomePage.Heading": "歡迎來到版面",
"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": "搜尋看板",
@ -369,12 +376,12 @@
"error.board-not-found": "沒有找到看板.",
"error.go-login": "登入",
"error.invalid-read-only-board": "沒有權限進入此看版.登入後才能訪問.",
"error.not-logged-in": "已被登出,請再次登入使用看板",
"error.not-logged-in": "已被登出,請再次登入使用看板",
"error.page.title": "很抱歉,發生了些錯誤",
"error.team-undefined": "不是有效的團隊",
"error.team-undefined": "不是有效的團隊",
"error.unknown": "發生一個錯誤。",
"generic.previous": "上一篇",
"guest-no-board.subtitle": "你尚未有權限進入此看板,請等人把你加入任何看板",
"guest-no-board.subtitle": "你尚未有權限進入此看板,請等人把你加入任何看板",
"guest-no-board.title": "尚未有看版",
"imagePaste.upload-failed": "有些檔案無法上傳.檔案大小達上限",
"limitedCard.title": "影藏卡片",
@ -386,7 +393,8 @@
"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.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": "權限",
@ -396,22 +404,34 @@
"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.channels-select-group": "頻道",
"shareBoard.confirm-change-team-role.body": "此看板上所有低於\"{role}\"的人都將<b>被提升到{role}</b>。你確定要改變這個看板最低角色?",
"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": "當你連接頻道到看板,該頻道所有成員(包含新的與現有的)都可以編輯,不包括訪客。",
"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": "了解",

View File

@ -1,6 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Utils} from './utils'
import {Card} from './blocks/card'
import {IPropertyTemplate, IPropertyOption, BoardGroup} from './blocks/board'
@ -17,7 +16,7 @@ function groupCardsByOptions(cards: Card[], optionIds: string[], groupByProperty
}
groups.push(group)
} else {
Utils.logError(`groupCardsByOptions: Missing option with id: ${optionId}`)
// if optionId not found, its an old (deleted) option that can be ignored
}
} else {
// Empty group

View File

@ -280,6 +280,10 @@ describe('src/cardFilter', () => {
}
test('verify isBefore clause', () => {
const filterClauseIsBeforeEmpty = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: []})
const resulta = CardFilter.isClauseMet(filterClauseIsBeforeEmpty, [template], dateCard)
expect(resulta).toBeTruthy()
const filterClauseIsBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [beforeRange.toString()]})
const result = CardFilter.isClauseMet(filterClauseIsBefore, [template], dateCard)
expect(result).toBeFalsy()
@ -294,6 +298,10 @@ describe('src/cardFilter', () => {
})
test('verify isAfter clauses', () => {
const filterClauseIsAfterEmpty = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: []})
const resulta = CardFilter.isClauseMet(filterClauseIsAfterEmpty, [template], dateCard)
expect(resulta).toBeTruthy()
const filterClauseIsAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [afterRange.toString()]})
const result = CardFilter.isClauseMet(filterClauseIsAfter, [template], dateCard)
expect(result).toBeFalsy()
@ -308,6 +316,10 @@ describe('src/cardFilter', () => {
})
test('verify is clause', () => {
const filterClauseIsEmpty = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: []})
const resulta = CardFilter.isClauseMet(filterClauseIsEmpty, [template], dateCard)
expect(resulta).toBeTruthy()
const filterClauseIsBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [beforeRange.toString()]})
const result = CardFilter.isClauseMet(filterClauseIsBefore, [template], dateCard)
expect(result).toBeFalsy()

View File

@ -175,6 +175,9 @@ class CardFilter {
return !(value as string || '').endsWith(filter.values[0]?.toLowerCase())
}
case 'isBefore': {
if (filter.values.length === 0) {
return true
}
if (dateValue !== undefined) {
const numericFilter = parseInt(filter.values[0], 10)
if (template && (template.type === 'createdTime' || template.type === 'updatedTime')) {
@ -192,6 +195,9 @@ class CardFilter {
return false
}
case 'isAfter': {
if (filter.values.length === 0) {
return true
}
if (dateValue !== undefined) {
const numericFilter = parseInt(filter.values[0], 10)
if (template && (template.type === 'createdTime' || template.type === 'updatedTime')) {

View File

@ -94,6 +94,7 @@ const GlobalHeaderSettingsMenu = (props: Props) => {
name={intl.formatMessage({id: 'Sidebar.random-icons', defaultMessage: 'Random icons'})}
isOn={randomIcons}
onClick={async () => toggleRandomIcons()}
suppressItemClicked={true}
/>
{me?.is_guest !== true &&
<Menu.Text

View File

@ -163,6 +163,7 @@ const SidebarSettingsMenu = (props: Props) => {
name={intl.formatMessage({id: 'Sidebar.random-icons', defaultMessage: 'Random icons'})}
isOn={randomIcons}
onClick={async () => toggleRandomIcons()}
suppressItemClicked={true}
/>
</Menu>
</MenuWrapper>

View File

@ -124,7 +124,7 @@
</ul>
<div class="secondary-footer__copy">
<small class="disclaimer"><span class="mm-copyright" style="margin-right: 1rem;">© Mattermost, Inc.
2022.</span> <span><a href="https://mattermost.com/terms-of-use/" title="Terms of Service">Terms
2023.</span> <span><a href="https://mattermost.com/terms-of-use/" title="Terms of Service">Terms
of Use</a> <span style="margin: 0 0.5rem;">|</span> <a
href="https://mattermost.com/privacy-policy/" title="Privacy Policy">Privacy Policy</a> <span
style="margin: 0 0.5rem;">|</span> <a href="https://mattermost.com/privacy-policy/cookies/"
@ -186,4 +186,4 @@
<script src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="{{ "js/tabs.js" | absURL }}"></script>
<script src="{{ "js/main.js" | absURL }}"></script>
<script src="{{ "js/main.js" | absURL }}"></script>