1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-02-07 19:30:18 +02:00

Adding the new template selector interface (#2172)

* Addinig initial version of a working template selector

* Some improvements

* Small improvements in the code

* More polishing

* Code reorganization

* Fixing tests

* Fixing linter errors

* Allowing to edit/delete templates

* Removing no longer needed code reducing race conditions

* Fixing some tests

* Adding some unit tests

* Adding more tests

* Splitting a bit more the board template selector for simplification

* Moving the delete dialog to the board template selector item

* Fixing some tests

* Fixing tests

* Exctracting i18n strings

* Trying to fix part of the cypress tests

* Fixing cypress tests

* Updating template selector UI

* Updating UI

* Updating padding

* Fixing css linter errors

* Fixing css error introduced in the previous commit

* Updating snapshots and fixing tests

* Fixing cypress tests again

* Adressing review comments

* Fixing tests

Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com>
This commit is contained in:
Jesús Espino 2022-02-02 09:38:57 +01:00 committed by GitHub
parent 65c783c270
commit dcf7600ca4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 3022 additions and 1571 deletions

View File

@ -27,5 +27,6 @@ declare namespace Cypress {
* @param {string} item - one of the template menu options, ex. 'Empty board'
*/
uiCreateBoard: (item: string) => Chainable
uiCreateEmptyBoard: () => Chainable
}
}

View File

@ -18,7 +18,7 @@ describe('Create and delete board / card', () => {
cy.contains('+ Add board').should('exist').click()
// Tests for template selector
cy.contains('Select a template').should('exist')
cy.contains('Use this template').should('exist')
// Some options are present
cy.contains('Meeting Notes').should('exist')
@ -26,7 +26,7 @@ describe('Create and delete board / card', () => {
cy.contains('Project Tasks').should('exist')
// Create empty board
cy.contains('Empty board').should('exist').click()
cy.contains('Create empty board').should('exist').click({force: true})
cy.get('.BoardComponent').should('exist')
cy.get('.Editable.title').invoke('attr', 'placeholder').should('contain', 'Untitled board')
@ -40,7 +40,7 @@ describe('Create and delete board / card', () => {
it('Can create and delete a board and a card', () => {
// Visit a page and create new empty board
cy.visit('/')
cy.uiCreateBoard('Empty board')
cy.uiCreateEmptyBoard()
// Change board title
cy.log('**Change board title**')
@ -90,7 +90,7 @@ describe('Create and delete board / card', () => {
// Create a card by clicking on the + button
cy.log('**Create a card by clicking on the + button**')
cy.get('.KanbanColumnHeader .Button .AddIcon').click()
cy.get('.KanbanColumnHeader button .AddIcon').click()
cy.get('.CardDetail').should('exist')
cy.get('.Dialog.dialog-back .wrapper').click({force: true})
@ -132,7 +132,7 @@ describe('Create and delete board / card', () => {
parent().
parent().
find('.MenuWrapper').
find('.Button.IconButton').
find('button.IconButton').
click({force: true})
cy.contains('Delete board').click({force: true})
cy.get('.DeleteBoardDialog button.danger').click({force: true})
@ -142,7 +142,7 @@ describe('Create and delete board / card', () => {
it('MM-T4433 Scrolls the kanban board when dragging card to edge', () => {
// Visit a page and create new empty board
cy.visit('/')
cy.uiCreateBoard('Empty board')
cy.uiCreateEmptyBoard()
// Create 10 empty groups
cy.log('**Create new empty groups**')

View File

@ -11,7 +11,7 @@ describe('Manage groups', () => {
it('MM-T4284 Adding a group', () => {
// Visit a page and create new empty board
cy.visit('/')
cy.uiCreateBoard('Empty board')
cy.uiCreateEmptyBoard()
cy.contains('+ Add a group').click({force: true})
cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').should('exist')
@ -26,7 +26,7 @@ describe('Manage groups', () => {
it('MM-T4285 Adding group color', () => {
// Visit a page and create new empty board
cy.visit('/')
cy.uiCreateBoard('Empty board')
cy.uiCreateEmptyBoard()
cy.contains('+ Add a group').click({force: true})
cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').should('exist')

View File

@ -102,8 +102,9 @@ Cypress.Commands.add('apiChangePassword', (userId: string, oldPassword: string,
Cypress.Commands.add('uiCreateNewBoard', (title?: string) => {
cy.log('**Create new empty board**')
cy.findByText('+ Add board').click()
cy.findByRole('button', {name: 'Empty board'}).click()
cy.get('.empty-board').first().click({force: true})
cy.findByPlaceholderText('Untitled board').should('exist')
cy.wait(10)
if (title) {
cy.log('**Rename board**')
cy.findByPlaceholderText('Untitled board').type(`${title}{enter}`)

View File

@ -8,5 +8,15 @@ Cypress.Commands.add('uiCreateBoard', (item: string) => {
cy.contains('+ Add board').should('be.visible').click()
return cy.contains(item).click().wait(1000)
cy.contains(item).click()
cy.contains('Use this template').click({force: true}).wait(1000)
})
Cypress.Commands.add('uiCreateEmptyBoard', () => {
cy.log('Create new empty board')
cy.contains('+ Add board').should('be.visible').click()
return cy.contains('Create empty board').click({force: true}).wait(1000)
})

View File

@ -9,6 +9,15 @@
"BoardComponent.show": "Show",
"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": "New template",
"BoardTemplateSelector.create-empty-board": "Create empty board",
"BoardTemplateSelector.delete-template": "Delete template",
"BoardTemplateSelector.description": "Choose a template to help you get started. Easily customize the template to fit your needs, or create an empty board to start from scratch.",
"BoardTemplateSelector.edit-template": "Edit",
"BoardTemplateSelector.plugin.no-content-description": "Add a board to the sidebar using any of the templates defined below or start from scratch.{lineBreak} Members of \"{workspaceName}\" will have access to boards created here.",
"BoardTemplateSelector.plugin.no-content-title": "Create a Board in {workspaceName}",
"BoardTemplateSelector.title": "Create a Board",
"BoardTemplateSelector.use-this-template": "Use this template",
"BoardsUnfurl.Remainder": "+{remainder} more",
"BoardsUnfurl.Updated": "Updated {time}",
"Calculations.Options.average.displayName": "Average",
@ -45,6 +54,11 @@
"Calculations.Options.range.label": "Range",
"Calculations.Options.sum.displayName": "Sum",
"Calculations.Options.sum.label": "Sum",
"CardBadges.title-checkboxes": "Checkboxes",
"CardBadges.title-comments": "Comments",
"CardBadges.title-description": "This card has a description",
"CardDetail.Follow": "Follow",
"CardDetail.Following": "Following",
"CardDetail.add-content": "Add content",
"CardDetail.add-icon": "Add icon",
"CardDetail.add-property": "+ Add a property",
@ -61,10 +75,10 @@
"CardDetailProperty.property-deleted": "Deleted {propertyName} Successfully!",
"CardDetailProperty.property-name-change-subtext": "type from \"{oldPropType}\" to \"{newPropType}\"",
"CardDetailProperty.property-type-change-subtext": "name to \"{newPropName}\"",
"CardDetail.Follow": "Follow",
"CardDetail.Following": "Following",
"CardDialog.copiedLink": "Copied!",
"CardDialog.copyLink": "Copy link",
"CardDialog.delete-confirmation-dialog-button-text": "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.",
"ColorOption.selectColor": "Select {color} Color",
@ -72,7 +86,6 @@
"CommentsList.send": "Send",
"ConfirmationDialog.cancel-action": "Cancel",
"ConfirmationDialog.confirm-action": "Confirm",
"ConfirmationDialog.delete-action": "Delete",
"ContentBlock.Delete": "Delete",
"ContentBlock.DeleteAction": "delete",
"ContentBlock.addElement": "add {type}",
@ -96,16 +109,9 @@
"DeleteBoardDialog.confirm-delete": "Delete",
"DeleteBoardDialog.confirm-info": "Are you sure you want to delete the board “{boardTitle}”? Deleting it will delete all cards in the board.",
"DeleteBoardDialog.confirm-tite": "Confirm Delete Board",
"DeleteBoardDialog.confirm-tite-template": "Confirm Delete Board Template",
"Dialog.closeDialog": "Close dialog",
"EditableDayPicker.today": "Today",
"EmptyCenterPanel.no-content": "Add or select a board from the sidebar to get started.",
"EmptyCenterPanel.plugin.choose-a-template": "Choose a template",
"EmptyCenterPanel.plugin.empty-board": "Start with an Empty Board",
"EmptyCenterPanel.plugin.end-message": "You can change the channel using the switcher in the sidebar.",
"EmptyCenterPanel.plugin.new-template": "New template",
"EmptyCenterPanel.plugin.no-content-description": "Add a board to the sidebar using any of the templates defined below or start from scratch.{lineBreak} Members of \"{workspaceName}\" will have access to boards created here.",
"EmptyCenterPanel.plugin.no-content-or": "or",
"EmptyCenterPanel.plugin.no-content-title": "Create a Board in {workspaceName}",
"Error.mobileweb": "Mobile web support is currently in early beta. Not all functionality may be present.",
"Error.websocket-closed": "Websocket connection closed, connection interrupted. If this persists, check your server or web proxy configuration.",
"Filter.includes": "includes",
@ -126,7 +132,6 @@
"KanbanCard.duplicate": "Duplicate",
"KanbanCard.untitled": "Untitled",
"Mutator.duplicate-board": "duplicate board",
"Mutator.new-board-from-template": "new board from template",
"Mutator.new-card-from-template": "new card from template",
"Mutator.new-template-from-board": "new template from board",
"Mutator.new-template-from-card": "new template from card",
@ -165,13 +170,9 @@
"ShareBoard.unshare": "Anyone with the link can view this board and all cards in it.",
"Sidebar.about": "About Focalboard",
"Sidebar.add-board": "+ Add board",
"Sidebar.add-template": "New template",
"Sidebar.changePassword": "Change password",
"Sidebar.delete-board": "Delete board",
"Sidebar.delete-template": "Delete",
"Sidebar.duplicate-board": "Duplicate board",
"Sidebar.edit-template": "Edit",
"Sidebar.empty-board": "Empty board",
"Sidebar.export-archive": "Export archive",
"Sidebar.import-archive": "Import archive",
"Sidebar.invite-users": "Invite users",
@ -179,12 +180,10 @@
"Sidebar.no-more-workspaces": "No more workspaces",
"Sidebar.no-views-in-board": "No pages inside",
"Sidebar.random-icons": "Random icons",
"Sidebar.select-a-template": "Select a template",
"Sidebar.set-language": "Set language",
"Sidebar.set-theme": "Set theme",
"Sidebar.settings": "Settings",
"Sidebar.template-from-board": "New template from board",
"Sidebar.untitled": "Untitled",
"Sidebar.untitled-board": "(Untitled Board)",
"Sidebar.untitled-view": "(Untitled View)",
"TableComponent.add-icon": "Add icon",
@ -211,7 +210,6 @@
"View.NewCalendarTitle": "Calendar view",
"View.NewGalleryTitle": "Gallery view",
"View.NewTableTitle": "Table view",
"View.NewTemplateTitle": "Untitled Template",
"View.Table": "Table",
"ViewHeader.add-template": "New template",
"ViewHeader.delete-template": "Delete",
@ -226,6 +224,7 @@
"ViewHeader.group-by": "Group by: {property}",
"ViewHeader.new": "New",
"ViewHeader.properties": "Properties",
"ViewHeader.properties-menu": "Properties menu",
"ViewHeader.search": "Search",
"ViewHeader.search-text": "Search text",
"ViewHeader.select-a-template": "Select a template",
@ -233,6 +232,7 @@
"ViewHeader.share-board": "Share board",
"ViewHeader.sort": "Sort",
"ViewHeader.untitled": "Untitled",
"ViewHeader.view-menu": "View menu",
"ViewTitle.hide-description": "hide description",
"ViewTitle.pick-icon": "Pick icon",
"ViewTitle.random-icon": "Random",
@ -246,6 +246,7 @@
"calendar.month": "Month",
"calendar.today": "TODAY",
"calendar.week": "Week",
"default-properties.badges": "Comments and Description",
"default-properties.title": "Title",
"error.no-workspace": "Your session may have expired or you may not have access to this workspace.",
"error.relogin": "Log in again",
@ -254,4 +255,4 @@
"login.register-button": "or create an account if you don't have one",
"register.login-button": "or log in if you already have an account",
"register.signup-title": "Sign up for your account"
}
}

56
webapp/src/boardUtils.ts Normal file
View File

@ -0,0 +1,56 @@
// 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'
export function groupCardsByOptions(cards: Card[], optionIds: string[], groupByProperty?: IPropertyTemplate): BoardGroup[] {
const groups = []
for (const optionId of optionIds) {
if (optionId) {
const option = groupByProperty?.options.find((o) => o.id === optionId)
if (option) {
const c = cards.filter((o) => optionId === o.fields.properties[groupByProperty!.id])
const group: BoardGroup = {
option,
cards: c,
}
groups.push(group)
} else {
Utils.logError(`groupCardsByOptions: Missing option with id: ${optionId}`)
}
} else {
// Empty group
const emptyGroupCards = cards.filter((card) => {
const groupByOptionId = card.fields.properties[groupByProperty?.id || '']
return !groupByOptionId || !groupByProperty?.options.find((option) => option.id === groupByOptionId)
})
const group: BoardGroup = {
option: {id: '', value: `No ${groupByProperty?.name}`, color: ''},
cards: emptyGroupCards,
}
groups.push(group)
}
}
return groups
}
export function getVisibleAndHiddenGroups(cards: Card[], visibleOptionIds: string[], hiddenOptionIds: string[], groupByProperty?: IPropertyTemplate): {visible: BoardGroup[], hidden: BoardGroup[]} {
let unassignedOptionIds: string[] = []
if (groupByProperty) {
unassignedOptionIds = groupByProperty.options.
filter((o: IPropertyOption) => !visibleOptionIds.includes(o.id) && !hiddenOptionIds.includes(o.id)).
map((o: IPropertyOption) => o.id)
}
const allVisibleOptionIds = [...visibleOptionIds, ...unassignedOptionIds]
// If the empty group positon is not explicitly specified, make it the first visible column
if (!allVisibleOptionIds.includes('') && !hiddenOptionIds.includes('')) {
allVisibleOptionIds.unshift('')
}
const visibleGroups = groupCardsByOptions(cards, allVisibleOptionIds, groupByProperty)
const hiddenGroups = groupCardsByOptions(cards, hiddenOptionIds, groupByProperty)
return {visible: visibleGroups, hidden: hiddenGroups}
}

View File

@ -105,7 +105,7 @@ exports[`components/blockIconSelector return menu on click 1`] = `
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"

View File

@ -17,7 +17,6 @@ exports[`components/cardDialog already following card 1`] = `
>
<button
aria-label="Close dialog"
class="Button IconButton IconButton--large"
title="Close dialog"
type="button"
>
@ -42,7 +41,6 @@ exports[`components/cardDialog already following card 1`] = `
role="button"
>
<button
class="Button IconButton IconButton--large"
type="button"
>
<i
@ -297,7 +295,6 @@ exports[`components/cardDialog return a cardDialog readonly 1`] = `
>
<button
aria-label="Close dialog"
class="Button IconButton IconButton--large"
title="Close dialog"
type="button"
>
@ -405,7 +402,6 @@ exports[`components/cardDialog return cardDialog menu content 1`] = `
>
<button
aria-label="Close dialog"
class="Button IconButton IconButton--large"
title="Close dialog"
type="button"
>
@ -430,7 +426,6 @@ exports[`components/cardDialog return cardDialog menu content 1`] = `
role="button"
>
<button
class="Button IconButton IconButton--large"
type="button"
>
<i
@ -452,7 +447,7 @@ exports[`components/cardDialog return cardDialog menu content 1`] = `
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -772,7 +767,6 @@ exports[`components/cardDialog return cardDialog menu content and cancel delete
>
<button
aria-label="Close dialog"
class="Button IconButton IconButton--large"
title="Close dialog"
type="button"
>
@ -797,7 +791,6 @@ exports[`components/cardDialog return cardDialog menu content and cancel delete
role="button"
>
<button
class="Button IconButton IconButton--large"
type="button"
>
<i
@ -1052,7 +1045,6 @@ exports[`components/cardDialog should match snapshot 1`] = `
>
<button
aria-label="Close dialog"
class="Button IconButton IconButton--large"
title="Close dialog"
type="button"
>
@ -1077,7 +1069,6 @@ exports[`components/cardDialog should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton IconButton--large"
type="button"
>
<i

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ exports[`/components/confirmationDialogBox confirmDialog should match snapshot 1
>
<button
aria-label="Close dialog"
class="Button IconButton IconButton--large"
class="IconButton size--medium"
title="Close dialog"
type="button"
>
@ -86,7 +86,7 @@ exports[`/components/confirmationDialogBox confirmDialog with Confirm Button Tex
>
<button
aria-label="Close dialog"
class="Button IconButton IconButton--large"
class="IconButton size--medium"
title="Close dialog"
type="button"
>

View File

@ -17,7 +17,6 @@ exports[`components/contentBlock return commentBlock and click on menuwrapper 1`
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -113,7 +112,7 @@ exports[`components/contentBlock return commentBlock and click on menuwrapper 1`
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -200,7 +199,6 @@ exports[`components/contentBlock should match snapshot with commentBlock 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -279,7 +277,6 @@ exports[`components/contentBlock should match snapshot with dividerBlock 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -338,7 +335,6 @@ exports[`components/contentBlock should match snapshot with imageBlock 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -399,7 +395,6 @@ exports[`components/contentBlock should match snapshot with textBlock 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i

View File

@ -17,7 +17,7 @@ exports[`components/dialog should match snapshot 1`] = `
>
<button
aria-label="Close dialog"
class="Button IconButton IconButton--large"
class="IconButton size--medium"
title="Close dialog"
type="button"
>
@ -52,7 +52,7 @@ exports[`components/dialog should return dialog and click on cancel button 1`] =
>
<button
aria-label="Close dialog"
class="Button IconButton IconButton--large"
class="IconButton size--medium"
title="Close dialog"
type="button"
>
@ -66,7 +66,7 @@ exports[`components/dialog should return dialog and click on cancel button 1`] =
role="button"
>
<button
class="Button IconButton IconButton--large"
class="IconButton size--medium"
type="button"
>
<i

View File

@ -1,141 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/emptyCenterPanel a focalboard Plugin should match snapshot 1`] = `
<div>
<div
class="EmptyCenterPanel"
>
<div
class="content"
>
<span
class="title"
>
Create a Board in Workspace 1
</span>
<span
class="description"
>
Add a board to the sidebar using any of the templates defined below or start from scratch.
<br />
Members of "
<b>
Workspace 1
</b>
" will have access to boards created here.
</span>
<span
class="choose-template-text"
>
Choose a template
</span>
<div
class="button-container"
>
<div
class="button "
>
<span>
🚴🏻‍♂️
</span>
<span
class="button-title"
>
Template 1
</span>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
</div>
<div
class="button "
>
<span>
🚴🏻‍♂️
</span>
<span
class="button-title"
>
Template Global
</span>
</div>
<div
class="button new-template"
>
<span>
<i
class="CompassIcon icon-plus AddIcon"
/>
</span>
<span
class="button-title"
>
New template
</span>
</div>
</div>
<span
class="choose-template-text"
>
or
</span>
<div
class="button "
>
<span>
<svg
class="BoardIcon Icon"
fill="currentColor"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<g
opacity="0.8"
>
<path
clip-rule="evenodd"
d="M4 4H20V20H4V4ZM2 4C2 2.89543 2.89543 2 4 2H20C21.1046 2 22 2.89543 22 4V20C22 21.1046 21.1046 22 20 22H4C2.89543 22 2 21.1046 2 20V4ZM8 6H6V12H8V6ZM11 6H13V16H11V6ZM18 6H16V9H18V6Z"
fill="currentColor"
fill-rule="evenodd"
/>
</g>
</svg>
</span>
<span
class="button-title"
>
Start with an Empty Board
</span>
</div>
You can change the channel using the switcher in the sidebar.
</div>
</div>
</div>
`;
exports[`components/emptyCenterPanel not a focalboard Plugin should match snapshot 1`] = `
<div>
<div
class="EmptyCenterPanel"
>
<div
class="Hint"
>
Add or select a board from the sidebar to get started.
</div>
</div>
</div>
`;

View File

@ -10,7 +10,7 @@ exports[`components/modal return Modal on position bottom 1`] = `
>
<button
aria-label="Close"
class="Button IconButton"
class="IconButton"
title="Close"
type="button"
>
@ -36,7 +36,7 @@ exports[`components/modal return Modal on position bottom-right 1`] = `
>
<button
aria-label="Close"
class="Button IconButton"
class="IconButton"
title="Close"
type="button"
>
@ -62,7 +62,7 @@ exports[`components/modal return Modal on position top 1`] = `
>
<button
aria-label="Close"
class="Button IconButton"
class="IconButton"
title="Close"
type="button"
>
@ -88,7 +88,7 @@ exports[`components/modal should match snapshot 1`] = `
>
<button
aria-label="Close"
class="Button IconButton"
class="IconButton"
title="Close"
type="button"
>

View File

@ -28,7 +28,7 @@ exports[`components/propertyValueElement URL fields should allow cancel 1`] = `
</a>
<button
aria-label="Edit"
class="Button IconButton Button_Edit"
class="IconButton Button_Edit"
title="Edit"
type="button"
>
@ -38,7 +38,7 @@ exports[`components/propertyValueElement URL fields should allow cancel 1`] = `
</button>
<button
aria-label="Copy"
class="Button IconButton Button_Copy"
class="IconButton Button_Copy"
title="Copy"
type="button"
>
@ -156,7 +156,7 @@ exports[`components/propertyValueElement should match snapshot, url, array value
</a>
<button
aria-label="Edit"
class="Button IconButton Button_Edit"
class="IconButton Button_Edit"
title="Edit"
type="button"
>
@ -166,7 +166,7 @@ exports[`components/propertyValueElement should match snapshot, url, array value
</button>
<button
aria-label="Copy"
class="Button IconButton Button_Copy"
class="IconButton Button_Copy"
title="Copy"
type="button"
>
@ -193,7 +193,7 @@ exports[`components/propertyValueElement should match snapshot, url, array value
</a>
<button
aria-label="Edit"
class="Button IconButton Button_Edit"
class="IconButton Button_Edit"
title="Edit"
type="button"
>
@ -203,7 +203,7 @@ exports[`components/propertyValueElement should match snapshot, url, array value
</button>
<button
aria-label="Copy"
class="Button IconButton Button_Copy"
class="IconButton Button_Copy"
title="Copy"
type="button"
>

View File

@ -10,7 +10,6 @@ exports[`src/components/shareBoardComponent return shareBoardComponent and click
>
<button
aria-label="Close"
class="Button IconButton"
title="Close"
type="button"
>
@ -81,7 +80,6 @@ exports[`src/components/shareBoardComponent return shareBoardComponent and click
>
<button
aria-label="Close"
class="Button IconButton"
title="Close"
type="button"
>
@ -152,7 +150,6 @@ exports[`src/components/shareBoardComponent return shareBoardComponent and click
>
<button
aria-label="Close"
class="Button IconButton"
title="Close"
type="button"
>
@ -223,7 +220,6 @@ exports[`src/components/shareBoardComponent return shareBoardComponent and click
>
<button
aria-label="Close"
class="Button IconButton"
title="Close"
type="button"
>
@ -294,7 +290,6 @@ exports[`src/components/shareBoardComponent should match snapshot 1`] = `
>
<button
aria-label="Close"
class="Button IconButton"
title="Close"
type="button"
>
@ -335,7 +330,6 @@ exports[`src/components/shareBoardComponent should match snapshot with sharing 1
>
<button
aria-label="Close"
class="Button IconButton"
title="Close"
type="button"
>
@ -406,7 +400,6 @@ exports[`src/components/shareBoardComponent should match snapshot with sharing a
>
<button
aria-label="Close"
class="Button IconButton"
title="Close"
type="button"
>
@ -477,7 +470,6 @@ exports[`src/components/shareBoardComponent should match snapshot with sharing a
>
<button
aria-label="Close"
class="Button IconButton"
title="Close"
type="button"
>
@ -548,7 +540,6 @@ exports[`src/components/shareBoardComponent should match snapshot with sharing a
>
<button
aria-label="Close"
class="Button IconButton"
title="Close"
type="button"
>

View File

@ -106,7 +106,7 @@ Object {
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -270,7 +270,7 @@ Object {
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"

View File

@ -72,7 +72,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
class="sidebarSwitcher"
>
<button
class="Button IconButton"
type="button"
>
<svg
@ -106,13 +105,13 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
class="CompassIcon icon-chevron-down ChevronDownIcon"
/>
</div>
<span
class="add-workspace-icon"
<button
type="button"
>
<i
class="CompassIcon icon-plus AddIcon"
/>
</span>
</button>
</div>
<div
class="octo-sidebar-list"
@ -124,7 +123,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
class="octo-sidebar-item ' expanded active"
>
<button
class="Button IconButton"
type="button"
>
<svg
@ -158,7 +156,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -202,19 +199,9 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
class="octo-spacer"
/>
<div
class="SidebarAddBoardMenu"
class="add-board"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="menu-entry"
>
+ Add board
</div>
</div>
+ Add board
</div>
<div
class="SidebarSettingsMenu"
@ -389,7 +376,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -471,7 +457,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -544,7 +529,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -553,7 +537,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
</button>
</div>
<button
class="Button IconButton"
type="button"
>
<i
@ -598,7 +581,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -607,7 +589,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
</button>
</div>
<button
class="Button IconButton"
type="button"
>
<i
@ -645,7 +626,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -689,7 +669,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -733,7 +712,6 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -940,7 +918,6 @@ exports[`src/components/workspace return workspace readonly and showcard 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -1138,7 +1115,7 @@ exports[`src/components/workspace return workspace readonly and showcard 1`] = `
</div>
`;
exports[`src/components/workspace return workspace with EmptyCenterPanel component 1`] = `
exports[`src/components/workspace return workspace with BoardTemplateSelector component 1`] = `
<div>
<div
class="Workspace"
@ -1147,12 +1124,76 @@ exports[`src/components/workspace return workspace with EmptyCenterPanel compone
class="mainFrame"
>
<div
class="EmptyCenterPanel"
class="BoardTemplateSelector"
>
<div
class="Hint"
class="toolbar"
/>
<div
class="header"
>
Add or select a board from the sidebar to get started.
<h1
class="title"
>
Create a Board in Workspace 1
</h1>
<p
class="description"
>
Add a board to the sidebar using any of the templates defined below or start from scratch.
<br />
Members of "
<b>
Workspace 1
</b>
" will have access to boards created here.
</p>
</div>
<div
class="templates"
>
<div
class="templates-list"
>
<div
class="new-template"
>
<span
class="template-icon"
>
<i
class="CompassIcon icon-plus AddIcon"
/>
</span>
<span
class="template-name"
>
New template
</span>
</div>
</div>
<div
class="template-preview-box"
>
<div
class="buttons"
>
<button
type="button"
>
<span>
Use this template
</span>
</button>
<button
type="button"
>
<span>
Create empty board
</span>
</button>
</div>
</div>
</div>
</div>
</div>
@ -1232,7 +1273,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
class="sidebarSwitcher"
>
<button
class="Button IconButton"
type="button"
>
<svg
@ -1266,13 +1306,13 @@ exports[`src/components/workspace should match snapshot 1`] = `
class="CompassIcon icon-chevron-down ChevronDownIcon"
/>
</div>
<span
class="add-workspace-icon"
<button
type="button"
>
<i
class="CompassIcon icon-plus AddIcon"
/>
</span>
</button>
</div>
<div
class="octo-sidebar-list"
@ -1284,7 +1324,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
class="octo-sidebar-item ' expanded active"
>
<button
class="Button IconButton"
type="button"
>
<svg
@ -1318,7 +1357,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -1362,19 +1400,9 @@ exports[`src/components/workspace should match snapshot 1`] = `
class="octo-spacer"
/>
<div
class="SidebarAddBoardMenu"
class="add-board"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="menu-entry"
>
+ Add board
</div>
</div>
+ Add board
</div>
<div
class="SidebarSettingsMenu"
@ -1549,7 +1577,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -1631,7 +1658,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -1704,7 +1730,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -1713,7 +1738,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
</button>
</div>
<button
class="Button IconButton"
type="button"
>
<i
@ -1758,7 +1782,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -1767,7 +1790,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
</button>
</div>
<button
class="Button IconButton"
type="button"
>
<i
@ -1805,7 +1827,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -1849,7 +1870,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -1893,7 +1913,6 @@ exports[`src/components/workspace should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -2100,7 +2119,6 @@ exports[`src/components/workspace should match snapshot with readonly 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i

View File

@ -0,0 +1,537 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/boardTemplateSelector/boardTemplateSelector a focalboard Plugin should match snapshot 1`] = `
<div>
<div
class="BoardTemplateSelector"
>
<div
class="toolbar"
>
<button
aria-label="Close"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="header"
>
<h1
class="title"
>
Create a Board
</h1>
<p
class="description"
>
Choose a template to help you get started. Easily customize the template to fit your needs, or create an empty board to start from scratch.
</p>
</div>
<div
class="templates"
>
<div
class="templates-list"
>
<div
class="BoardTemplateSelectorItem active"
>
<span
class="template-icon"
>
🚴🏻‍♂️
</span>
<span
class="template-name"
>
Template Global
</span>
</div>
<div
class="BoardTemplateSelectorItem"
>
<span
class="template-icon"
>
🚴🏻‍♂️
</span>
<span
class="template-name"
>
Template 1
</span>
<div
class="actions"
>
<button
aria-label="Delete"
title="Delete"
type="button"
>
<i
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
</button>
<button
aria-label="Edit"
title="Edit"
type="button"
>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
</button>
</div>
</div>
<div
class="new-template"
>
<span
class="template-icon"
>
<i
class="CompassIcon icon-plus AddIcon"
/>
</span>
<span
class="template-name"
>
New template
</span>
</div>
</div>
<div
class="template-preview-box"
>
<div
class="BoardTemplateSelectorPreview"
>
<div
class="prevent-click"
/>
</div>
<div
class="buttons"
>
<button
type="button"
>
<span>
Use this template
</span>
</button>
<button
type="button"
>
<span>
Create empty board
</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/boardTemplateSelector/boardTemplateSelector a focalboard Plugin should match snapshot with custom title and description 1`] = `
<div>
<div
class="BoardTemplateSelector"
>
<div
class="toolbar"
/>
<div
class="header"
>
<h1
class="title"
>
test-title
</h1>
<p
class="description"
>
test-description
</p>
</div>
<div
class="templates"
>
<div
class="templates-list"
>
<div
class="BoardTemplateSelectorItem active"
>
<span
class="template-icon"
>
🚴🏻‍♂️
</span>
<span
class="template-name"
>
Template Global
</span>
</div>
<div
class="BoardTemplateSelectorItem"
>
<span
class="template-icon"
>
🚴🏻‍♂️
</span>
<span
class="template-name"
>
Template 1
</span>
<div
class="actions"
>
<button
aria-label="Delete"
title="Delete"
type="button"
>
<i
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
</button>
<button
aria-label="Edit"
title="Edit"
type="button"
>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
</button>
</div>
</div>
<div
class="new-template"
>
<span
class="template-icon"
>
<i
class="CompassIcon icon-plus AddIcon"
/>
</span>
<span
class="template-name"
>
New template
</span>
</div>
</div>
<div
class="template-preview-box"
>
<div
class="BoardTemplateSelectorPreview"
>
<div
class="prevent-click"
/>
</div>
<div
class="buttons"
>
<button
type="button"
>
<span>
Use this template
</span>
</button>
<button
type="button"
>
<span>
Create empty board
</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/boardTemplateSelector/boardTemplateSelector a focalboard Plugin should match snapshot without close 1`] = `
<div>
<div
class="BoardTemplateSelector"
>
<div
class="toolbar"
/>
<div
class="header"
>
<h1
class="title"
>
Create a Board
</h1>
<p
class="description"
>
Choose a template to help you get started. Easily customize the template to fit your needs, or create an empty board to start from scratch.
</p>
</div>
<div
class="templates"
>
<div
class="templates-list"
>
<div
class="BoardTemplateSelectorItem active"
>
<span
class="template-icon"
>
🚴🏻‍♂️
</span>
<span
class="template-name"
>
Template Global
</span>
</div>
<div
class="BoardTemplateSelectorItem"
>
<span
class="template-icon"
>
🚴🏻‍♂️
</span>
<span
class="template-name"
>
Template 1
</span>
<div
class="actions"
>
<button
aria-label="Delete"
title="Delete"
type="button"
>
<i
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
</button>
<button
aria-label="Edit"
title="Edit"
type="button"
>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
</button>
</div>
</div>
<div
class="new-template"
>
<span
class="template-icon"
>
<i
class="CompassIcon icon-plus AddIcon"
/>
</span>
<span
class="template-name"
>
New template
</span>
</div>
</div>
<div
class="template-preview-box"
>
<div
class="BoardTemplateSelectorPreview"
>
<div
class="prevent-click"
/>
</div>
<div
class="buttons"
>
<button
type="button"
>
<span>
Use this template
</span>
</button>
<button
type="button"
>
<span>
Create empty board
</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/boardTemplateSelector/boardTemplateSelector not a focalboard Plugin should match snapshot 1`] = `
<div>
<div
class="BoardTemplateSelector"
>
<div
class="toolbar"
>
<button
aria-label="Close"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="header"
>
<h1
class="title"
>
Create a Board
</h1>
<p
class="description"
>
Choose a template to help you get started. Easily customize the template to fit your needs, or create an empty board to start from scratch.
</p>
</div>
<div
class="templates"
>
<div
class="templates-list"
>
<div
class="BoardTemplateSelectorItem active"
>
<span
class="template-icon"
>
🚴🏻‍♂️
</span>
<span
class="template-name"
>
Template Global
</span>
</div>
<div
class="BoardTemplateSelectorItem"
>
<span
class="template-icon"
>
🚴🏻‍♂️
</span>
<span
class="template-name"
>
Template 1
</span>
<div
class="actions"
>
<button
aria-label="Delete"
title="Delete"
type="button"
>
<i
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
</button>
<button
aria-label="Edit"
title="Edit"
type="button"
>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
</button>
</div>
</div>
<div
class="new-template"
>
<span
class="template-icon"
>
<i
class="CompassIcon icon-plus AddIcon"
/>
</span>
<span
class="template-name"
>
New template
</span>
</div>
</div>
<div
class="template-preview-box"
>
<div
class="BoardTemplateSelectorPreview"
>
<div
class="prevent-click"
/>
</div>
<div
class="buttons"
>
<button
type="button"
>
<span>
Use this template
</span>
</button>
<button
type="button"
>
<span>
Create empty board
</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,205 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/boardTemplateSelector/boardTemplateSelectorItem should match snapshot 1`] = `
<div>
<div
class="BoardTemplateSelectorItem"
>
<span
class="template-icon"
>
🚴🏻‍♂️
</span>
<span
class="template-name"
>
Template 1
</span>
<div
class="actions"
>
<button
aria-label="Delete"
title="Delete"
type="button"
>
<i
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
</button>
<button
aria-label="Edit"
title="Edit"
type="button"
>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
</button>
</div>
</div>
</div>
`;
exports[`components/boardTemplateSelector/boardTemplateSelectorItem should match snapshot when active 1`] = `
<div>
<div
class="BoardTemplateSelectorItem active"
>
<span
class="template-icon"
>
🚴🏻‍♂️
</span>
<span
class="template-name"
>
Template 1
</span>
<div
class="actions"
>
<button
aria-label="Delete"
title="Delete"
type="button"
>
<i
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
</button>
<button
aria-label="Edit"
title="Edit"
type="button"
>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
</button>
</div>
</div>
</div>
`;
exports[`components/boardTemplateSelector/boardTemplateSelectorItem should match snapshot with global template 1`] = `
<div>
<div
class="BoardTemplateSelectorItem"
>
<span
class="template-icon"
>
🚴🏻‍♂️
</span>
<span
class="template-name"
>
Template global
</span>
</div>
</div>
`;
exports[`components/boardTemplateSelector/boardTemplateSelectorItem should trigger the onDelete (and not any other) when click the delete icon and confirm 1`] = `
<div
id="focalboard-root-portal"
>
<div
class="BoardTemplateSelectorItem"
>
<span
class="template-icon"
>
🚴🏻‍♂️
</span>
<span
class="template-name"
>
Template 1
</span>
<div
class="actions"
>
<button
aria-label="Delete"
title="Delete"
type="button"
>
<i
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
</button>
<button
aria-label="Edit"
title="Edit"
type="button"
>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
</button>
</div>
</div>
<div>
<div
class="Dialog dialog-back DeleteBoardDialog"
>
<div
class="wrapper"
>
<div
class="dialog"
role="dialog"
>
<div
class="toolbar"
>
<button
aria-label="Close dialog"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="container"
>
<h2
class="header text-heading5"
>
Confirm Delete Board Template
</h2>
<p
class="body"
>
Are you sure you want to delete the board template “Template 1”?
</p>
<div
class="footer"
>
<button
type="button"
>
<span>
Cancel
</span>
</button>
<button
type="button"
>
<span>
Delete
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,436 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/boardTemplateSelector/boardTemplateSelectorPreview should be null without activeTemplate 1`] = `<div />`;
exports[`components/boardTemplateSelector/boardTemplateSelectorPreview should match snapshot 1`] = `
<div>
<div
class="BoardTemplateSelectorPreview"
>
<div
class="prevent-click"
/>
<div
class="top-head"
>
<div
class="ViewTitle"
>
<div
class="add-buttons add-visible"
/>
<div
class="title"
>
<div
class="BlockIconSelector"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="octo-icon size-m"
>
<span>
🚴🏻‍♂️
</span>
</div>
</div>
</div>
<input
class="Editable readonly title"
placeholder="Untitled board"
readonly=""
spellcheck="true"
title="Template 1"
value="Template 1"
/>
</div>
</div>
<div
class="ViewHeader"
>
<input
class="Editable undefined"
placeholder="Untitled View"
spellcheck="true"
title="View"
value="View"
/>
<div
aria-label="View menu"
class="MenuWrapper"
role="button"
>
<button
type="button"
>
<i
class="CompassIcon icon-chevron-down DropdownIcon"
/>
</button>
</div>
<div
class="octo-spacer"
/>
<div
aria-label="Properties menu"
class="MenuWrapper"
role="button"
>
<button
type="button"
>
<span>
Properties
</span>
</button>
</div>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
type="button"
>
<span>
Group by:
<span
id="groupByLabel"
>
name
</span>
</span>
</button>
</div>
<div
class="ModalWrapper"
>
<button
type="button"
>
<span>
Filter
</span>
</button>
</div>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
type="button"
>
<span>
Sort
</span>
</button>
</div>
<button
type="button"
>
<span>
Search
</span>
</button>
<div
class="ModalWrapper"
>
<div
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
<button
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
</div>
<div
class="ButtonWithMenu"
>
<div
class="button-text"
>
New
</div>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="button-dropdown"
>
<i
class="CompassIcon icon-chevron-down DropdownIcon"
/>
</div>
</div>
</div>
</div>
</div>
<div
class="Kanban"
>
<div
class="octo-board-header"
id="mainBoardHeader"
>
<div
class="octo-board-header-cell KanbanColumnHeader"
draggable="true"
style="opacity: 1;"
>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
>
No name
</span>
<div
class="KanbanCalculation"
>
<button
title="1"
type="button"
>
<span>
1
</span>
</button>
</div>
<div
class="octo-spacer"
/>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
<button
type="button"
>
<i
class="CompassIcon icon-plus AddIcon"
/>
</button>
</div>
<div
class="octo-board-header-cell KanbanColumnHeader"
draggable="true"
style="opacity: 1;"
>
<span
class="Label propColorOrange "
>
<input
class="Editable undefined"
placeholder="New Select"
spellcheck="true"
title="Q1"
value="Q1"
/>
</span>
<div
class="KanbanCalculation"
>
<button
title="0"
type="button"
>
<span>
0
</span>
</button>
</div>
<div
class="octo-spacer"
/>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
<button
type="button"
>
<i
class="CompassIcon icon-plus AddIcon"
/>
</button>
</div>
<div
class="octo-board-header-cell KanbanColumnHeader"
draggable="true"
style="opacity: 1;"
>
<span
class="Label propColorBlue "
>
<input
class="Editable undefined"
placeholder="New Select"
spellcheck="true"
title="Q2"
value="Q2"
/>
</span>
<div
class="KanbanCalculation"
>
<button
title="0"
type="button"
>
<span>
0
</span>
</button>
</div>
<div
class="octo-spacer"
/>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
<button
type="button"
>
<i
class="CompassIcon icon-plus AddIcon"
/>
</button>
</div>
<div
class="octo-board-header-cell narrow"
>
<button
type="button"
>
<span>
+ Add a group
</span>
</button>
</div>
</div>
<div
class="octo-board-body"
id="mainBoardBody"
>
<div
class="octo-board-column"
>
<div
class="KanbanCard"
draggable="true"
style="opacity: 1;"
>
<div
aria-label="menuwrapper"
class="MenuWrapper optionsMenu"
role="button"
>
<button
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
<div
class="octo-icontitle"
>
<div
class="octo-icon"
>
🚴🏻‍♂️
</div>
<div
class="octo-titletext"
>
Card
</div>
</div>
<div
class="octo-tooltip tooltip-top"
data-tooltip="name"
>
<div
class="octo-propertyvalue octo-propertyvalue--readonly"
>
test
</div>
</div>
</div>
<button
type="button"
>
<span>
+ New
</span>
</button>
</div>
<div
class="octo-board-column"
>
<button
type="button"
>
<span>
+ New
</span>
</button>
</div>
<div
class="octo-board-column"
>
<button
type="button"
>
<span>
+ New
</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,92 @@
.BoardTemplateSelector {
position: absolute;
background-color: rgb(var(--center-channel-bg-rgb));
z-index: 1000;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: auto;
padding-bottom: 48px;
.toolbar {
display: flex;
width: 100%;
justify-content: flex-end;
padding: 24px;
}
.header {
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
.title {
font-size: 25px;
}
.description {
max-width: 640px;
text-align: center;
margin: 0 0 32px;
}
}
.templates {
display: flex;
width: 100%;
padding: 0 10%;
justify-content: center;
.templates-list {
margin-right: 32px;
min-width: 200px;
max-height: 500px;
overflow-y: auto;
.new-template {
position: relative;
display: flex;
align-items: center;
padding: 8px 16px;
margin-bottom: 4px;
border-radius: var(--default-rad);
cursor: pointer;
.template-icon {
margin-right: 10px;
}
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.1);
}
}
}
.template-preview-box {
position: relative;
background-color: rgb(var(--center-channel-bg-rgb));
box-shadow: rgba(var(--center-channel-color-rgb), 0.1) 0 0 0 1px,
rgba(var(--center-channel-color-rgb), 0.1) 0 2px 4px;
border-radius: var(--modal-rad);
overflow: hidden;
width: 100%;
max-width: 1000px;
.buttons {
display: flex;
position: absolute;
bottom: 32px;
width: 100%;
justify-content: center;
}
.Button {
&:first-child {
margin-right: 16px;
}
}
}
}
}

View File

@ -0,0 +1,280 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {render, screen, act, waitFor, within} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
import {MockStoreEnhanced} from 'redux-mock-store'
import {createMemoryHistory} from 'history'
import {mocked} from 'ts-jest/utils'
import {Provider as ReduxProvider} from 'react-redux'
import {MemoryRouter, Router} from 'react-router-dom'
import Mutator from '../../mutator'
import {Utils} from '../../utils'
import {UserWorkspace} from '../../user'
import {mockDOM, mockStateStore, wrapDNDIntl} from '../../testUtils'
import BoardTemplateSelector from './boardTemplateSelector'
jest.mock('react-router-dom', () => {
const originalModule = jest.requireActual('react-router-dom')
return {
...originalModule,
useRouteMatch: jest.fn(() => {
return {url: '/'}
}),
}
})
jest.mock('../../octoClient', () => {
return {
getSubtree: jest.fn(() => Promise.resolve([])),
}
})
jest.mock('../../utils')
jest.mock('../../mutator')
describe('components/boardTemplateSelector/boardTemplateSelector', () => {
const mockedUtils = mocked(Utils, true)
const mockedMutator = mocked(Mutator, true)
const workspace1: UserWorkspace = {
id: 'workspace_1',
title: 'Workspace 1',
boardCount: 1,
}
const template1Title = 'Template 1'
const globalTemplateTitle = 'Template Global'
const boardTitle = 'Board 1'
let store:MockStoreEnhanced<unknown, unknown>
beforeAll(mockDOM)
beforeEach(() => {
jest.clearAllMocks()
const state = {
workspace: {
userWorkspaces: new Array<UserWorkspace>(workspace1),
current: workspace1,
},
boards: {
boards: [
{
id: '2',
title: boardTitle,
workspaceId: workspace1.id,
fields: {
icon: '🚴🏻‍♂️',
cardProperties: [
{id: 'id-6'},
],
dateDisplayPropertyId: 'id-6',
},
},
],
templates: [
{
id: '1',
workspaceId: workspace1.id,
title: template1Title,
fields: {
icon: '🚴🏻‍♂️',
cardProperties: [
{id: 'id-5'},
],
dateDisplayPropertyId: 'id-5',
},
},
],
cards: [],
views: [],
},
globalTemplates: {
value: [{
id: 'global-1',
title: globalTemplateTitle,
workspaceId: '0',
fields: {
icon: '🚴🏻‍♂️',
cardProperties: [
{id: 'global-id-5'},
],
dateDisplayPropertyId: 'global-id-5',
},
}],
},
}
store = mockStateStore([], state)
})
describe('not a focalboard Plugin', () => {
beforeAll(() => {
mockedUtils.isFocalboardPlugin.mockReturnValue(false)
})
test('should match snapshot', () => {
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
expect(container).toMatchSnapshot()
})
})
describe('a focalboard Plugin', () => {
beforeAll(() => {
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
})
test('should match snapshot', () => {
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
expect(container).toMatchSnapshot()
})
test('should match snapshot without close', () => {
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
expect(container).toMatchSnapshot()
})
test('should match snapshot with custom title and description', () => {
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector
title='test-title'
description='test-description'
/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
expect(container).toMatchSnapshot()
})
test('return BoardTemplateSelector and click close call the onClose callback', () => {
const onClose = jest.fn()
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={onClose}/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
const divCloseButton = container.querySelector('div.toolbar .CloseIcon')
expect(divCloseButton).not.toBeNull()
userEvent.click(divCloseButton!)
expect(onClose).toBeCalledTimes(1)
})
test('return BoardTemplateSelector and click new template', () => {
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
const divNewTemplate = container.querySelector('div.new-template')
expect(divNewTemplate).not.toBeNull()
userEvent.click(divNewTemplate!)
expect(mockedMutator.addEmptyBoardTemplate).toBeCalledTimes(1)
})
test('return BoardTemplateSelector and click empty board', () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
const divEmptyboard = screen.getByText('Create empty board').parentElement
expect(divEmptyboard).not.toBeNull()
userEvent.click(divEmptyboard!)
expect(mockedMutator.addEmptyBoard).toBeCalledTimes(1)
})
test('return BoardTemplateSelector and click delete template icon', async () => {
const root = document.createElement('div')
root.setAttribute('id', 'focalboard-root-portal')
render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
</ReduxProvider>
,
), {wrapper: MemoryRouter, container: document.body.appendChild(root)})
const deleteIcon = screen.getByText(template1Title).parentElement?.querySelector('.DeleteIcon')
expect(deleteIcon).not.toBeNull()
act(() => {
userEvent.click(deleteIcon!)
})
const {getByText} = within(root)
const deleteConfirm = getByText('Delete')
expect(deleteConfirm).not.toBeNull()
await act(async () => {
await userEvent.click(deleteConfirm!)
})
expect(mockedMutator.deleteBlock).toBeCalledTimes(1)
})
test('return BoardTemplateSelector and click edit template icon', async () => {
const history = createMemoryHistory()
history.push = jest.fn()
render(wrapDNDIntl(
<Router history={history}>
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
</ReduxProvider>
</Router>,
))
const editIcon = screen.getByText(template1Title).parentElement?.querySelector('.EditIcon')
expect(editIcon).not.toBeNull()
userEvent.click(editIcon!)
expect(history.push).toBeCalledTimes(1)
})
test('return BoardTemplateSelector and click to add board from template', async () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
const divBoardToSelect = screen.getByText(template1Title).parentElement
expect(divBoardToSelect).not.toBeNull()
act(() => {
userEvent.click(divBoardToSelect!)
})
const useTemplateButton = screen.getByText('Use this template').parentElement
expect(useTemplateButton).not.toBeNull()
act(() => {
userEvent.click(useTemplateButton!)
})
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledTimes(1))
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(expect.anything(), expect.anything(), expect.anything(), expect.anything(), false))
})
test('return BoardTemplateSelector and click to add board from global template', async () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
const divBoardToSelect = screen.getByText(globalTemplateTitle).parentElement
expect(divBoardToSelect).not.toBeNull()
act(() => {
userEvent.click(divBoardToSelect!)
})
const useTemplateButton = screen.getByText('Use this template').parentElement
expect(useTemplateButton).not.toBeNull()
act(() => {
userEvent.click(useTemplateButton!)
})
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledTimes(1))
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(expect.anything(), expect.anything(), expect.anything(), expect.anything(), true))
})
})
})

View File

@ -0,0 +1,172 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useState, useCallback, useMemo} from 'react'
import {FormattedMessage, useIntl} from 'react-intl'
import {generatePath, useHistory, useRouteMatch} from 'react-router-dom'
import {Board} from '../../blocks/board'
import IconButton from '../../widgets/buttons/iconButton'
import CloseIcon from '../../widgets/icons/close'
import AddIcon from '../../widgets/icons/add'
import Button from '../../widgets/buttons/button'
import octoClient from '../../octoClient'
import mutator from '../../mutator'
import {getTemplates, getCurrentBoard} from '../../store/boards'
import {fetchGlobalTemplates, getGlobalTemplates} from '../../store/globalTemplates'
import {useAppDispatch, useAppSelector} from '../../store/hooks'
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
import BoardTemplateSelectorPreview from './boardTemplateSelectorPreview'
import BoardTemplateSelectorItem from './boardTemplateSelectorItem'
import './boardTemplateSelector.scss'
type Props = {
title?: React.ReactNode
description?: React.ReactNode
onClose?: () => void
}
const BoardTemplateSelector = React.memo((props: Props) => {
const globalTemplates = useAppSelector<Board[]>(getGlobalTemplates) || []
const currentBoard = useAppSelector<Board>(getCurrentBoard) || null
const {title, description, onClose} = props
const dispatch = useAppDispatch()
const intl = useIntl()
const history = useHistory()
const match = useRouteMatch<{boardId: string, viewId?: string}>()
const showBoard = useCallback(async (boardId) => {
const params = {...match.params, boardId: boardId || ''}
delete params.viewId
const newPath = generatePath(match.path, params)
history.push(newPath)
if (onClose) {
onClose()
}
}, [match, history, onClose])
useEffect(() => {
if (octoClient.workspaceId !== '0' && globalTemplates.length === 0) {
dispatch(fetchGlobalTemplates())
}
}, [octoClient.workspaceId])
const onBoardTemplateDelete = useCallback((template: Board) => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DeleteBoardTemplate, {board: template.id})
mutator.deleteBlock(
template,
intl.formatMessage({id: 'BoardTemplateSelector.delete-template', defaultMessage: 'Delete template'}),
async () => {
},
async () => {
showBoard(template.id)
},
)
}, [showBoard])
const unsortedTemplates = useAppSelector(getTemplates)
const templates = useMemo(() => Object.values(unsortedTemplates).sort((a: Board, b: Board) => a.createAt - b.createAt), [unsortedTemplates])
const allTemplates = globalTemplates.concat(templates)
const [activeTemplate, setActiveTemplate] = useState<Board>(allTemplates[0])
useEffect(() => {
if (!activeTemplate) {
setActiveTemplate(templates.concat(globalTemplates)[0])
}
}, [templates, globalTemplates])
if (!allTemplates) {
return <div/>
}
return (
<div className='BoardTemplateSelector'>
<div className='toolbar'>
{onClose &&
<IconButton
size='medium'
onClick={onClose}
icon={<CloseIcon/>}
title={'Close'}
/>}
</div>
<div className='header'>
<h1 className='title'>
{title || (
<FormattedMessage
id='BoardTemplateSelector.title'
defaultMessage='Create a Board'
/>
)}
</h1>
<p className='description'>
{description || (
<FormattedMessage
id='BoardTemplateSelector.description'
defaultMessage='Choose a template to help you get started. Easily customize the template to fit your needs, or create an empty board to start from scratch.'
/>
)}
</p>
</div>
<div className='templates'>
<div className='templates-list'>
{allTemplates.map((boardTemplate) => (
<BoardTemplateSelectorItem
key={boardTemplate.id}
isActive={activeTemplate?.id === boardTemplate.id}
template={boardTemplate}
onSelect={setActiveTemplate}
onDelete={onBoardTemplateDelete}
onEdit={showBoard}
/>
))}
<div
className='new-template'
onClick={() => mutator.addEmptyBoardTemplate(intl, showBoard, () => showBoard(currentBoard.id))}
>
<span className='template-icon'><AddIcon/></span>
<span className='template-name'>
<FormattedMessage
id='BoardTemplateSelector.add-template'
defaultMessage='New template'
/>
</span>
</div>
</div>
<div className='template-preview-box'>
<BoardTemplateSelectorPreview activeTemplate={activeTemplate}/>
<div className='buttons'>
<Button
filled={true}
size={'medium'}
onClick={() => mutator.addBoardFromTemplate(intl, showBoard, () => showBoard(currentBoard.id), activeTemplate.id, activeTemplate.workspaceId === '0')}
>
<FormattedMessage
id='BoardTemplateSelector.use-this-template'
defaultMessage='Use this template'
/>
</Button>
<Button
className='empty-board'
filled={false}
emphasis={'secondary'}
size={'medium'}
onClick={() => mutator.addEmptyBoard(intl, showBoard, () => showBoard(currentBoard.id))}
>
<FormattedMessage
id='BoardTemplateSelector.create-empty-board'
defaultMessage='Create empty board'
/>
</Button>
</div>
</div>
</div>
</div>
)
})
export default BoardTemplateSelector

View File

@ -0,0 +1,45 @@
.BoardTemplateSelectorItem {
cursor: pointer;
transition: all 100ms ease-out 0s;
font-weight: 600;
position: relative;
display: flex;
align-items: center;
padding: 8px 16px;
margin-bottom: 4px;
border-radius: var(--default-rad);
color: rgba(var(--center-channel-color-rgb), 0.64);
height: 40px;
.template-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.template-icon {
margin-right: 10px;
}
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 1);
.actions {
display: flex;
}
}
&.active {
background-color: rgba(var(--button-bg-rgb), 0.08);
color: rgba(var(--button-bg-rgb), 1);
}
.actions {
position: relative;
display: none;
right: -8px;
}
}

View File

@ -0,0 +1,254 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {render, within, act, waitFor} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
import {Board, IPropertyTemplate} from '../../blocks/board'
import {wrapDNDIntl} from '../../testUtils'
import BoardTemplateSelectorItem from './boardTemplateSelectorItem'
jest.mock('react-router-dom', () => {
const originalModule = jest.requireActual('react-router-dom')
return {
...originalModule,
useRouteMatch: jest.fn(() => {
return {url: '/'}
}),
}
})
const groupProperty: IPropertyTemplate = {
id: 'group-prop-id',
name: 'name',
type: 'text',
options: [
{
color: 'propColorOrange',
id: 'property_value_id_1',
value: 'Q1',
},
{
color: 'propColorBlue',
id: 'property_value_id_2',
value: 'Q2',
},
],
}
jest.mock('../../octoClient', () => {
return {
getSubtree: jest.fn(() => Promise.resolve([
{
id: '1',
workspaceId: 'workspace',
title: 'Template',
type: 'board',
fields: {
icon: '🚴🏻‍♂️',
cardProperties: [groupProperty],
dateDisplayPropertyId: 'id-5',
},
},
{
id: '2',
workspaceId: 'workspace',
title: 'View',
type: 'view',
fields: {
groupById: 'group-prop-id',
viewType: 'board',
visibleOptionIds: ['group-prop-id'],
hiddenOptionIds: [],
visiblePropertyIds: ['group-prop-id'],
sortOptions: [],
kanbanCalculations: {},
},
},
{
id: '3',
workspaceId: 'workspace',
title: 'Card',
type: 'card',
fields: {
icon: '🚴🏻‍♂️',
properties: {
'group-prop-id': 'test',
},
},
},
])),
}
})
jest.mock('../../utils')
jest.mock('../../mutator')
describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => {
const template: Board = {
id: '1',
workspaceId: 'workspace_1',
title: 'Template 1',
createdBy: 'user-1',
modifiedBy: 'user-1',
createAt: 10,
updateAt: 20,
deleteAt: 0,
type: 'board',
parentId: '123',
rootId: '123',
schema: 1,
fields: {
description: 'test',
icon: '🚴🏻‍♂️',
cardProperties: [groupProperty],
dateDisplayPropertyId: 'id-5',
columnCalculations: {},
},
}
const globalTemplate: Board = {
id: 'global-1',
title: 'Template global',
workspaceId: '0',
createdBy: 'user-1',
modifiedBy: 'user-1',
createAt: 10,
updateAt: 20,
deleteAt: 0,
type: 'board',
parentId: '123',
rootId: '123',
schema: 1,
fields: {
icon: '🚴🏻‍♂️',
description: 'test',
cardProperties: [groupProperty],
dateDisplayPropertyId: 'global-id-5',
columnCalculations: {},
},
}
beforeEach(() => {
jest.clearAllMocks()
})
test('should match snapshot', async () => {
const {container} = render(wrapDNDIntl(
<BoardTemplateSelectorItem
isActive={false}
template={template}
onSelect={jest.fn()}
onDelete={jest.fn()}
onEdit={jest.fn()}
/>
,
))
expect(container).toMatchSnapshot()
})
test('should match snapshot when active', async () => {
const {container} = render(wrapDNDIntl(
<BoardTemplateSelectorItem
isActive={true}
template={template}
onSelect={jest.fn()}
onDelete={jest.fn()}
onEdit={jest.fn()}
/>
,
))
expect(container).toMatchSnapshot()
})
test('should match snapshot with global template', async () => {
const {container} = render(wrapDNDIntl(
<BoardTemplateSelectorItem
isActive={false}
template={globalTemplate}
onSelect={jest.fn()}
onDelete={jest.fn()}
onEdit={jest.fn()}
/>
,
))
expect(container).toMatchSnapshot()
})
test('should trigger the onSelect (and not any other) when click the element', async () => {
const onSelect = jest.fn()
const onDelete = jest.fn()
const onEdit = jest.fn()
const {container} = render(wrapDNDIntl(
<BoardTemplateSelectorItem
isActive={false}
template={template}
onSelect={onSelect}
onDelete={onDelete}
onEdit={onEdit}
/>
,
))
userEvent.click(container.querySelector('.BoardTemplateSelectorItem')!)
expect(onSelect).toBeCalledTimes(1)
expect(onSelect).toBeCalledWith(template)
expect(onDelete).not.toBeCalled()
expect(onEdit).not.toBeCalled()
})
test('should trigger the onDelete (and not any other) when click the delete icon', async () => {
const onSelect = jest.fn()
const onDelete = jest.fn()
const onEdit = jest.fn()
const {container} = render(wrapDNDIntl(
<BoardTemplateSelectorItem
isActive={false}
template={template}
onSelect={onSelect}
onDelete={onDelete}
onEdit={onEdit}
/>
,
))
userEvent.click(container.querySelector('.BoardTemplateSelectorItem .EditIcon')!)
expect(onEdit).toBeCalledTimes(1)
expect(onEdit).toBeCalledWith(template.id)
expect(onSelect).not.toBeCalled()
expect(onDelete).not.toBeCalled()
})
test('should trigger the onDelete (and not any other) when click the delete icon and confirm', async () => {
const onSelect = jest.fn()
const onDelete = jest.fn()
const onEdit = jest.fn()
const root = document.createElement('div')
root.setAttribute('id', 'focalboard-root-portal')
render(wrapDNDIntl(
<BoardTemplateSelectorItem
isActive={false}
template={template}
onSelect={onSelect}
onDelete={onDelete}
onEdit={onEdit}
/>
,
), {container: document.body.appendChild(root)})
act(() => {
userEvent.click(root.querySelector('.BoardTemplateSelectorItem .DeleteIcon')!)
})
expect(root).toMatchSnapshot()
const {getByText} = within(root)
act(() => {
userEvent.click(getByText('Delete')!)
})
await waitFor(async () => expect(onDelete).toBeCalledTimes(1))
await waitFor(async () => expect(onDelete).toBeCalledWith(template))
await waitFor(async () => expect(onSelect).not.toBeCalled())
await waitFor(async () => expect(onEdit).not.toBeCalled())
})
})

View File

@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useState} from 'react'
import {useIntl} from 'react-intl'
import {Board} from '../../blocks/board'
import IconButton from '../../widgets/buttons/iconButton'
import DeleteIcon from '../../widgets/icons/delete'
import EditIcon from '../../widgets/icons/edit'
import DeleteBoardDialog from '../sidebar/deleteBoardDialog'
import './boardTemplateSelectorItem.scss'
type Props = {
isActive: boolean
template: Board
onSelect: (template: Board) => void
onDelete: (template: Board) => void
onEdit: (templateId: string) => void
}
const BoardTemplateSelectorItem = React.memo((props: Props) => {
const {isActive, template, onEdit, onDelete, onSelect} = props
const intl = useIntl()
const [deleteOpen, setDeleteOpen] = useState<boolean>(false)
const onClickHandler = useCallback(() => {
onSelect(template)
}, [onSelect, template])
const onEditHandler = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
onEdit(template.id)
}, [onEdit, template])
return (
<div
className={isActive ? 'BoardTemplateSelectorItem active' : 'BoardTemplateSelectorItem'}
onClick={onClickHandler}
>
<span className='template-icon'>{template.fields.icon}</span>
<span className='template-name'>{template.title}</span>
{template.workspaceId !== '0' &&
<div className='actions'>
<IconButton
icon={<DeleteIcon/>}
title={intl.formatMessage({id: 'BoardTemplateSelector.delete-template', defaultMessage: 'Delete'})}
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
setDeleteOpen(true)
}}
/>
<IconButton
icon={<EditIcon/>}
title={intl.formatMessage({id: 'BoardTemplateSelector.edit-template', defaultMessage: 'Edit'})}
onClick={onEditHandler}
/>
</div>}
{deleteOpen &&
<DeleteBoardDialog
boardTitle={template.title}
onClose={() => setDeleteOpen(false)}
isTemplate={true}
onDelete={async () => {
onDelete(template)
}}
/>}
</div>
)
})
export default BoardTemplateSelectorItem

View File

@ -0,0 +1,18 @@
.BoardTemplateSelectorPreview {
position: relative;
transform: scale(0.6) translateX(-30%) translateY(-30%);
width: 158%;
height: 480px;
border-radius: var(--modal-rad);
.Kanban {
overflow: hidden;
}
.prevent-click {
position: absolute;
width: 100%;
height: 100%;
z-index: 1100;
}
}

View File

@ -0,0 +1,180 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {render, waitFor} from '@testing-library/react'
import React from 'react'
import {MockStoreEnhanced} from 'redux-mock-store'
import {Provider as ReduxProvider} from 'react-redux'
import {UserWorkspace} from '../../user'
import {IPropertyTemplate} from '../../blocks/board'
import {mockDOM, mockStateStore, wrapDNDIntl} from '../../testUtils'
import BoardTemplateSelectorPreview from './boardTemplateSelectorPreview'
jest.mock('react-router-dom', () => {
const originalModule = jest.requireActual('react-router-dom')
return {
...originalModule,
useRouteMatch: jest.fn(() => {
return {url: '/'}
}),
}
})
const groupProperty: IPropertyTemplate = {
id: 'group-prop-id',
name: 'name',
type: 'text',
options: [
{
color: 'propColorOrange',
id: 'property_value_id_1',
value: 'Q1',
},
{
color: 'propColorBlue',
id: 'property_value_id_2',
value: 'Q2',
},
],
}
jest.mock('../../octoClient', () => {
return {
getSubtree: jest.fn(() => Promise.resolve([
{
id: '1',
workspaceId: 'workspace',
title: 'Template',
type: 'board',
fields: {
icon: '🚴🏻‍♂️',
cardProperties: [groupProperty],
dateDisplayPropertyId: 'id-5',
},
},
{
id: '2',
workspaceId: 'workspace',
title: 'View',
type: 'view',
fields: {
groupById: 'group-prop-id',
viewType: 'board',
visibleOptionIds: ['group-prop-id'],
hiddenOptionIds: [],
visiblePropertyIds: ['group-prop-id'],
sortOptions: [],
kanbanCalculations: {},
},
},
{
id: '3',
workspaceId: 'workspace',
title: 'Card',
type: 'card',
fields: {
icon: '🚴🏻‍♂️',
properties: {
'group-prop-id': 'test',
},
},
},
])),
}
})
jest.mock('../../utils')
jest.mock('../../mutator')
describe('components/boardTemplateSelector/boardTemplateSelectorPreview', () => {
const workspace1: UserWorkspace = {
id: 'workspace_1',
title: 'Workspace 1',
boardCount: 1,
}
const template1Title = 'Template 1'
const globalTemplateTitle = 'Template Global'
const boardTitle = 'Board 1'
let store:MockStoreEnhanced<unknown, unknown>
beforeAll(mockDOM)
beforeEach(() => {
jest.clearAllMocks()
const state = {
searchText: {value: ''},
users: {me: {id: 'user-id'}},
cards: {templates: []},
views: {views: []},
contents: {contents: []},
comments: {comments: []},
workspace: {
userWorkspaces: new Array<UserWorkspace>(workspace1),
current: workspace1,
},
boards: {
boards: [
{
id: '2',
title: boardTitle,
workspaceId: workspace1.id,
fields: {
icon: '🚴🏻‍♂️',
cardProperties: [groupProperty],
dateDisplayPropertyId: 'id-6',
},
},
],
templates: [
{
id: '1',
workspaceId: workspace1.id,
title: template1Title,
fields: {
icon: '🚴🏻‍♂️',
cardProperties: [groupProperty],
dateDisplayPropertyId: 'id-5',
},
},
],
cards: [],
views: [],
},
globalTemplates: {
value: [{
id: 'global-1',
title: globalTemplateTitle,
workspaceId: '0',
fields: {
icon: '🚴🏻‍♂️',
cardProperties: [
{id: 'global-id-5'},
],
dateDisplayPropertyId: 'global-id-5',
},
}],
},
}
store = mockStateStore([], state)
})
test('should match snapshot', async () => {
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelectorPreview activeTemplate={(store.getState() as any).boards.templates[0]}/>
</ReduxProvider>
,
))
await waitFor(() => expect(container.querySelector('.top-head')).not.toBeNull())
expect(container).toMatchSnapshot()
})
test('should be null without activeTemplate', () => {
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelectorPreview activeTemplate={null}/>
</ReduxProvider>
,
))
expect(container).toMatchSnapshot()
})
})

View File

@ -0,0 +1,145 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useState, useMemo} from 'react'
import {Board} from '../../blocks/board'
import {Card} from '../../blocks/card'
import {BoardView} from '../../blocks/boardView'
import octoClient from '../../octoClient'
import {getVisibleAndHiddenGroups} from '../../boardUtils'
import ViewHeader from '../viewHeader/viewHeader'
import ViewTitle from '../viewTitle'
import Kanban from '../kanban/kanban'
import Table from '../table/table'
import CalendarFullView from '../calendar/fullCalendar'
import Gallery from '../gallery/gallery'
import './boardTemplateSelectorPreview.scss'
type Props = {
activeTemplate: Board|null
}
const BoardTemplateSelectorPreview = React.memo((props: Props) => {
const {activeTemplate} = props
const [activeView, setActiveView] = useState<BoardView|null>(null)
const [activeTemplateCards, setActiveTemplateCards] = useState<Card[]>([])
useEffect(() => {
if (activeTemplate) {
setActiveTemplateCards([])
setActiveView(null)
setActiveTemplateCards([])
octoClient.getSubtree(activeTemplate.id, activeView?.fields.viewType === 'gallery' ? 3 : 2, activeTemplate.workspaceId).then((blocks) => {
const cards = blocks.filter((b) => b.type === 'card')
const views = blocks.filter((b) => b.type === 'view')
if (views.length > 0) {
setActiveView(views[0] as BoardView)
}
if (cards.length > 0) {
setActiveTemplateCards(cards as Card[])
}
})
}
}, [activeTemplate])
const dateDisplayProperty = useMemo(() => {
return activeTemplate?.fields.cardProperties.find((o) => o.id === activeView?.fields.dateDisplayPropertyId)
}, [activeView, activeTemplate])
const groupByProperty = useMemo(() => {
return activeTemplate?.fields.cardProperties.find((o) => o.id === activeView?.fields.groupById) || activeTemplate?.fields.cardProperties[0]
}, [activeView, activeTemplate])
const {visible: visibleGroups, hidden: hiddenGroups} = useMemo(() => {
if (!activeView) {
return {visible: [], hidden: []}
}
return getVisibleAndHiddenGroups(activeTemplateCards, activeView.fields.visibleOptionIds, activeView?.fields.hiddenOptionIds, groupByProperty)
}, [activeTemplateCards, activeView, groupByProperty])
if (!activeTemplate) {
return null
}
return (
<div className='BoardTemplateSelectorPreview'>
<div className='prevent-click'/>
{activeView &&
<div className='top-head'>
<ViewTitle
key={activeTemplate?.id + activeTemplate?.title}
board={activeTemplate}
readonly={true}
/>
<ViewHeader
board={activeTemplate}
activeView={activeView}
cards={activeTemplateCards}
views={[activeView]}
groupByProperty={groupByProperty}
addCard={() => null}
addCardFromTemplate={() => null}
addCardTemplate={() => null}
editCardTemplate={() => null}
readonly={false}
showShared={false}
/>
</div>}
{activeView?.fields.viewType === 'board' &&
<Kanban
board={activeTemplate}
activeView={activeView}
cards={activeTemplateCards}
groupByProperty={groupByProperty}
visibleGroups={visibleGroups}
hiddenGroups={hiddenGroups}
selectedCardIds={[]}
readonly={false}
onCardClicked={() => null}
addCard={() => Promise.resolve()}
showCard={() => null}
/>}
{activeView?.fields.viewType === 'table' &&
<Table
board={activeTemplate}
activeView={activeView}
cards={activeTemplateCards}
groupByProperty={groupByProperty}
views={[activeView]}
visibleGroups={visibleGroups}
selectedCardIds={[]}
readonly={false}
cardIdToFocusOnRender={''}
onCardClicked={() => null}
addCard={() => Promise.resolve()}
showCard={() => null}
/>}
{activeView?.fields.viewType === 'gallery' &&
<Gallery
board={activeTemplate}
cards={activeTemplateCards}
activeView={activeView}
readonly={false}
selectedCardIds={[]}
onCardClicked={() => null}
addCard={() => Promise.resolve()}
/>}
{activeView?.fields.viewType === 'calendar' &&
<CalendarFullView
board={activeTemplate}
cards={activeTemplateCards}
activeView={activeView}
readonly={false}
dateDisplayProperty={dateDisplayProperty}
showCard={() => null}
addCard={() => Promise.resolve()}
/>}
</div>
)
})
export default BoardTemplateSelectorPreview

View File

@ -98,7 +98,7 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -162,7 +162,7 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -217,7 +217,7 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -287,7 +287,7 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -347,7 +347,7 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -420,7 +420,7 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -480,7 +480,7 @@ exports[`components/cardDetail/cardDetailContents should match snapshot after dr
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -553,7 +553,7 @@ exports[`components/cardDetail/cardDetailContents should match snapshot with con
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -626,7 +626,7 @@ exports[`components/cardDetail/cardDetailContents should match snapshot with con
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -690,7 +690,7 @@ exports[`components/cardDetail/cardDetailContents should match snapshot with con
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -745,7 +745,7 @@ exports[`components/cardDetail/cardDetailContents should match snapshot with con
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i

View File

@ -31,7 +31,7 @@ exports[`components/cardDetail/comment return comment 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -53,7 +53,7 @@ exports[`components/cardDetail/comment return comment 1`] = `
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -135,7 +135,7 @@ exports[`components/cardDetail/comment return comment and delete comment 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -157,7 +157,7 @@ exports[`components/cardDetail/comment return comment and delete comment 1`] = `
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"

View File

@ -11,7 +11,7 @@ import {ClientConfig} from '../config/clientConfig'
import {Block} from '../blocks/block'
import {BlockIcons} from '../blockIcons'
import {Card, createCard} from '../blocks/card'
import {Board, IPropertyTemplate, IPropertyOption, BoardGroup} from '../blocks/board'
import {Board, IPropertyTemplate} from '../blocks/board'
import {BoardView} from '../blocks/boardView'
import {CardFilter} from '../cardFilter'
import mutator from '../mutator'
@ -19,6 +19,7 @@ import {Utils} from '../utils'
import {UserSettings} from '../userSettings'
import {addCard, addTemplate} from '../store/cards'
import {updateView} from '../store/views'
import {getVisibleAndHiddenGroups} from '../boardUtils'
import './centerPanel.scss'
@ -114,7 +115,7 @@ class CenterPanel extends React.Component<Props, State> {
render(): JSX.Element {
const {groupByProperty, activeView, board, views, cards} = this.props
const {visible: visibleGroups, hidden: hiddenGroups} = this.getVisibleAndHiddenGroups(cards, activeView.fields.visibleOptionIds, activeView.fields.hiddenOptionIds, groupByProperty)
const {visible: visibleGroups, hidden: hiddenGroups} = getVisibleAndHiddenGroups(cards, activeView.fields.visibleOptionIds, activeView.fields.hiddenOptionIds, groupByProperty)
return (
<div
@ -400,55 +401,6 @@ class CenterPanel extends React.Component<Props, State> {
this.setState({selectedCardIds: []})
}
private groupCardsByOptions(cards: Card[], optionIds: string[], groupByProperty?: IPropertyTemplate): BoardGroup[] {
const groups = []
for (const optionId of optionIds) {
if (optionId) {
const option = groupByProperty?.options.find((o) => o.id === optionId)
if (option) {
const c = cards.filter((o) => optionId === o.fields.properties[groupByProperty!.id])
const group: BoardGroup = {
option,
cards: c,
}
groups.push(group)
} else {
Utils.logError(`groupCardsByOptions: Missing option with id: ${optionId}`)
}
} else {
// Empty group
const emptyGroupCards = cards.filter((card) => {
const groupByOptionId = card.fields.properties[groupByProperty?.id || '']
return !groupByOptionId || !groupByProperty?.options.find((option) => option.id === groupByOptionId)
})
const group: BoardGroup = {
option: {id: '', value: `No ${groupByProperty?.name}`, color: ''},
cards: emptyGroupCards,
}
groups.push(group)
}
}
return groups
}
private getVisibleAndHiddenGroups(cards: Card[], visibleOptionIds: string[], hiddenOptionIds: string[], groupByProperty?: IPropertyTemplate): {visible: BoardGroup[], hidden: BoardGroup[]} {
let unassignedOptionIds: string[] = []
if (groupByProperty) {
unassignedOptionIds = groupByProperty.options.
filter((o: IPropertyOption) => !visibleOptionIds.includes(o.id) && !hiddenOptionIds.includes(o.id)).
map((o: IPropertyOption) => o.id)
}
const allVisibleOptionIds = [...visibleOptionIds, ...unassignedOptionIds]
// If the empty group positon is not explicitly specified, make it the first visible column
if (!allVisibleOptionIds.includes('') && !hiddenOptionIds.includes('')) {
allVisibleOptionIds.unshift('')
}
const visibleGroups = this.groupCardsByOptions(cards, allVisibleOptionIds, groupByProperty)
const hiddenGroups = this.groupCardsByOptions(cards, hiddenOptionIds, groupByProperty)
return {visible: visibleGroups, hidden: hiddenGroups}
}
}
export default connect(undefined, {addCard, addTemplate, updateView})(injectIntl(CenterPanel))

View File

@ -36,6 +36,7 @@ const Dialog = React.memo((props: Props) => {
<div
className='wrapper'
onClick={(e) => {
e.stopPropagation()
if (e.target === e.currentTarget) {
props.onClose()
}
@ -52,13 +53,13 @@ const Dialog = React.memo((props: Props) => {
onClick={props.onClose}
icon={<CloseIcon/>}
title={closeDialogText}
className='IconButton--large'
size='medium'
/>
}
{toolbar && <div className='cardToolbar'>{toolbar}</div>}
{toolsMenu && <MenuWrapper>
<IconButton
className='IconButton--large'
size='medium'
icon={<OptionsIcon/>}
/>
{toolsMenu}

View File

@ -1,90 +0,0 @@
.EmptyCenterPanel {
display: flex;
flex-direction: column;
overflow: auto;
padding: 80px;
font-size: 15px;
color: rgba(var(--center-channel-color-rgb));
.WorkspaceInfo {
b {
padding-left: 5px;
}
}
.content {
display: flex;
flex-direction: column;
justify-items: center;
align-items: center;
.title {
font-size: 25px;
color: rgba(var(--center-channel-color-rgb));
margin-bottom: 12px;
}
.description {
text-align: center;
margin-bottom: 32px;
color: rgba(var(--center-channel-color-rgb));
}
.choose-template-text {
color: var(--center-channel-color);
font-weight: 600;
}
.button-container {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.button {
display: flex;
justify-items: center;
align-items: center;
padding: 16px 32px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
box-sizing: border-box;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.08);
border-radius: 8px;
margin: 20px;
max-width: 300px;
span {
margin-right: 10px;
}
.button-title {
font-weight: 600;
margin-right: 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
svg {
display: flex;
}
&:hover {
cursor: pointer;
}
}
.new-template {
border: transparent;
box-shadow: none;
color: var(--button-bg);
i {
background: var(--button-bg);
color: var(--button-color);
margin: 0;
}
}
}
}

View File

@ -1,147 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {render, screen, waitFor} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
import {MockStoreEnhanced} from 'redux-mock-store'
import {mocked} from 'ts-jest/utils'
import {Provider as ReduxProvider} from 'react-redux'
import {MemoryRouter} from 'react-router-dom'
import Mutator from '../mutator'
import {Utils} from '../utils'
import {UserWorkspace} from '../user'
import {mockDOM, mockStateStore, wrapDNDIntl} from '../testUtils'
import EmptyCenterPanel from './emptyCenterPanel'
jest.mock('react-router-dom', () => {
const originalModule = jest.requireActual('react-router-dom')
return {
...originalModule,
useRouteMatch: jest.fn(() => {
return {url: '/'}
}),
}
})
jest.mock('../utils')
jest.mock('../mutator')
describe('components/emptyCenterPanel', () => {
const mockedUtils = mocked(Utils, true)
const mockedMutator = mocked(Mutator, true)
const workspace1: UserWorkspace = {
id: 'workspace_1',
title: 'Workspace 1',
boardCount: 1,
}
const template1Title = 'Template 1'
const globalTemplateTitle = 'Template Global'
let store:MockStoreEnhanced<unknown, unknown>
beforeAll(mockDOM)
beforeEach(() => {
jest.clearAllMocks()
const state = {
workspace: {
userWorkspaces: new Array<UserWorkspace>(workspace1),
current: workspace1,
},
boards: {
templates: [
{id: '1', title: template1Title, fields: {icon: '🚴🏻‍♂️'}},
],
},
globalTemplates: {
value: [{id: 'global-1', title: globalTemplateTitle, fields: {icon: '🚴🏻‍♂️'}}],
},
}
store = mockStateStore([], state)
})
describe('not a focalboard Plugin', () => {
beforeAll(() => {
mockedUtils.isFocalboardPlugin.mockReturnValue(false)
})
test('should match snapshot', () => {
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<EmptyCenterPanel/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
expect(container).toMatchSnapshot()
})
})
describe('a focalboard Plugin', () => {
beforeAll(() => {
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
})
test('should match snapshot', () => {
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<EmptyCenterPanel/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
expect(container).toMatchSnapshot()
})
test('return emptyCenterPanel and click new template', () => {
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<EmptyCenterPanel/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
const divNewTemplate = container.querySelector('div.button.new-template')
expect(divNewTemplate).not.toBeNull()
userEvent.click(divNewTemplate!)
expect(mockedMutator.insertBlocks).toBeCalledTimes(1)
})
test('return emptyCenterPanel and click empty board', () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<EmptyCenterPanel/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
const divEmptyboard = screen.getByText('Start with an Empty Board').parentElement
expect(divEmptyboard).not.toBeNull()
userEvent.click(divEmptyboard!)
expect(mockedMutator.insertBlocks).toBeCalledTimes(1)
})
test('return emptyCenterPanel and click to add board from template', async () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<EmptyCenterPanel/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
const divAddBoard = screen.getByText(template1Title).parentElement
expect(divAddBoard).not.toBeNull()
userEvent.click(divAddBoard!)
await waitFor(async () => {
expect(mockedMutator.duplicateBoard).toBeCalledTimes(1)
})
})
test('return emptyCenterPanel and click to add board from global template', async () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<EmptyCenterPanel/>
</ReduxProvider>
,
), {wrapper: MemoryRouter})
const divAddBoard = screen.getByText(globalTemplateTitle).parentElement
expect(divAddBoard).not.toBeNull()
userEvent.click(divAddBoard!)
await waitFor(async () => {
expect(mockedMutator.duplicateFromRootBoard).toBeCalledTimes(1)
})
})
})
})

View File

@ -1,173 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect} from 'react'
import {FormattedMessage, useIntl} from 'react-intl'
import {generatePath, useHistory, useRouteMatch} from 'react-router-dom'
import {getCurrentWorkspace} from '../store/workspace'
import {useAppSelector, useAppDispatch} from '../store/hooks'
import {Utils} from '../utils'
import {Board} from '../blocks/board'
import {getGlobalTemplates, fetchGlobalTemplates} from '../store/globalTemplates'
import {getSortedTemplates} from '../store/boards'
import AddIcon from '../widgets/icons/add'
import BoardIcon from '../widgets/icons/board'
import octoClient from '../octoClient'
import {addBoardTemplateClicked, addBoardClicked} from './sidebar/sidebarAddBoardMenu'
import {addBoardFromTemplate, BoardTemplateButtonMenu} from './sidebar/boardTemplateMenuItem'
import './emptyCenterPanel.scss'
type ButtonProps = {
buttonIcon: string | React.ReactNode,
title: string,
readonly: boolean,
onClick: () => void,
showBoard?: (boardId: string) => void
boardTemplate?: Board
classNames?: string
}
const PanelButton = React.memo((props: ButtonProps) => {
const {onClick, buttonIcon, title, readonly, showBoard, boardTemplate, classNames} = props
return (
<div
onClick={onClick}
className={`button ${classNames || ''}`}
>
<span>{buttonIcon}</span>
<span className='button-title'>{title}</span>
{!readonly && showBoard && boardTemplate &&
<BoardTemplateButtonMenu
showBoard={showBoard}
boardTemplate={boardTemplate}
/>
}
</div>
)
})
const EmptyCenterPanel = React.memo(() => {
const workspace = useAppSelector(getCurrentWorkspace)
const templates = useAppSelector(getSortedTemplates)
const globalTemplates = useAppSelector<Board[]>(getGlobalTemplates)
const history = useHistory()
const dispatch = useAppDispatch()
const intl = useIntl()
const match = useRouteMatch<{boardId: string, viewId?: string}>()
useEffect(() => {
if (octoClient.workspaceId !== '0' && globalTemplates.length === 0) {
dispatch(fetchGlobalTemplates())
}
}, [octoClient.workspaceId])
const showBoard = useCallback((boardId) => {
const params = {...match.params, boardId: boardId || ''}
delete params.viewId
const newPath = generatePath(match.path, params)
history.push(newPath)
}, [match, history])
const newTemplateClicked = () => addBoardTemplateClicked(showBoard, intl)
const emptyBoardClicked = () => addBoardClicked(showBoard, intl)
if (!Utils.isFocalboardPlugin()) {
return (
<div className='EmptyCenterPanel'>
<div className='Hint'>
<FormattedMessage
id='EmptyCenterPanel.no-content'
defaultMessage='Add or select a board from the sidebar to get started.'
/>
</div>
</div>
)
}
return (
<div className='EmptyCenterPanel'>
<div className='content'>
<span className='title'>
<FormattedMessage
id='EmptyCenterPanel.plugin.no-content-title'
defaultMessage='Create a Board in {workspaceName}'
values={{workspaceName: workspace?.title}}
/>
</span>
<span className='description'>
<FormattedMessage
id='EmptyCenterPanel.plugin.no-content-description'
defaultMessage='Add a board to the sidebar using any of the templates defined below or start from scratch.{lineBreak} Members of "{workspaceName}" will have access to boards created here.'
values={{
workspaceName: <b>{workspace?.title}</b>,
lineBreak: <br/>,
}}
/>
</span>
<span className='choose-template-text'>
<FormattedMessage
id='EmptyCenterPanel.plugin.choose-a-template'
defaultMessage='Choose a template'
/>
</span>
<div className='button-container'>
{templates.map((template) =>
(
<PanelButton
key={template.id}
title={template.title}
buttonIcon={template.fields.icon}
readonly={false}
onClick={() => addBoardFromTemplate(intl, showBoard, template.id)}
showBoard={showBoard}
boardTemplate={template}
/>
),
)}
{globalTemplates.map((template) =>
(
<PanelButton
key={template.id}
title={template.title}
buttonIcon={template.fields.icon}
readonly={true}
onClick={() => addBoardFromTemplate(intl, showBoard, template.id, undefined, true)}
/>
),
)}
<PanelButton
key={'new-template'}
title={intl.formatMessage({id: 'EmptyCenterPanel.plugin.new-template', defaultMessage: 'New template'})}
buttonIcon={<AddIcon/>}
readonly={true}
onClick={newTemplateClicked}
classNames='new-template'
/>
</div>
<span className='choose-template-text'>
<FormattedMessage
id='EmptyCenterPanel.plugin.no-content-or'
defaultMessage='or'
/>
</span>
<PanelButton
key={'start-with-an-empty-board'}
title={intl.formatMessage({id: 'EmptyCenterPanel.plugin.empty-board', defaultMessage: 'Start with an Empty Board'})}
buttonIcon={<BoardIcon/>}
readonly={true}
onClick={emptyBoardClicked}
/>
<FormattedMessage
id='EmptyCenterPanel.plugin.end-message'
defaultMessage='You can change the channel using the switcher in the sidebar.'
/>
</div>
</div>
)
})
export default EmptyCenterPanel

View File

@ -16,7 +16,7 @@ exports[`src/components/gallery/Gallery return Gallery and click new 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -46,7 +46,7 @@ exports[`src/components/gallery/Gallery return Gallery and click new 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -117,7 +117,7 @@ exports[`src/components/gallery/Gallery should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -139,7 +139,7 @@ exports[`src/components/gallery/Gallery should match snapshot 1`] = `
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -234,7 +234,7 @@ exports[`src/components/gallery/Gallery should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i

View File

@ -39,7 +39,6 @@ exports[`src/components/gallery/GalleryCard with a comment content should match
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -61,7 +60,7 @@ exports[`src/components/gallery/GalleryCard with a comment content should match
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -166,7 +165,6 @@ exports[`src/components/gallery/GalleryCard with an image content should match s
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -188,7 +186,7 @@ exports[`src/components/gallery/GalleryCard with an image content should match s
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -329,7 +327,6 @@ exports[`src/components/gallery/GalleryCard with many contents should match snap
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -351,7 +348,7 @@ exports[`src/components/gallery/GalleryCard with many contents should match snap
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -460,7 +457,6 @@ exports[`src/components/gallery/GalleryCard with many images content should matc
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -482,7 +478,7 @@ exports[`src/components/gallery/GalleryCard with many images content should matc
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -593,7 +589,6 @@ exports[`src/components/gallery/GalleryCard without block content return Gallery
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -633,7 +628,6 @@ exports[`src/components/gallery/GalleryCard without block content return Gallery
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -673,7 +667,6 @@ exports[`src/components/gallery/GalleryCard without block content return Gallery
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -713,7 +706,6 @@ exports[`src/components/gallery/GalleryCard without block content return Gallery
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -753,7 +745,6 @@ exports[`src/components/gallery/GalleryCard without block content should match s
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -775,7 +766,7 @@ exports[`src/components/gallery/GalleryCard without block content should match s
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"

View File

@ -52,7 +52,6 @@ exports[`src/component/kanban/kanban return kanban and change title on KanbanCol
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -61,7 +60,6 @@ exports[`src/component/kanban/kanban return kanban and change title on KanbanCol
</button>
</div>
<button
class="Button IconButton"
type="button"
>
<i
@ -106,7 +104,6 @@ exports[`src/component/kanban/kanban return kanban and change title on KanbanCol
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -115,7 +112,6 @@ exports[`src/component/kanban/kanban return kanban and change title on KanbanCol
</button>
</div>
<button
class="Button IconButton"
type="button"
>
<i
@ -158,7 +154,6 @@ exports[`src/component/kanban/kanban return kanban and change title on KanbanCol
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -192,7 +187,6 @@ exports[`src/component/kanban/kanban return kanban and change title on KanbanCol
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -237,7 +231,6 @@ exports[`src/component/kanban/kanban return kanban and change title on KanbanCol
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -504,7 +497,6 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -513,7 +505,6 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
</button>
</div>
<button
class="Button IconButton"
type="button"
>
<i
@ -558,7 +549,6 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -567,7 +557,6 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
</button>
</div>
<button
class="Button IconButton"
type="button"
>
<i
@ -610,7 +599,6 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -644,7 +632,6 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -689,7 +676,6 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -797,7 +783,6 @@ exports[`src/component/kanban/kanban should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -806,7 +791,6 @@ exports[`src/component/kanban/kanban should match snapshot 1`] = `
</button>
</div>
<button
class="Button IconButton"
type="button"
>
<i
@ -851,7 +835,6 @@ exports[`src/component/kanban/kanban should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -860,7 +843,6 @@ exports[`src/component/kanban/kanban should match snapshot 1`] = `
</button>
</div>
<button
class="Button IconButton"
type="button"
>
<i
@ -903,7 +885,6 @@ exports[`src/component/kanban/kanban should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -937,7 +918,6 @@ exports[`src/component/kanban/kanban should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -982,7 +962,6 @@ exports[`src/component/kanban/kanban should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i

View File

@ -13,7 +13,6 @@ exports[`src/components/kanban/kanbanCard return kanbanCard and click on copy li
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -35,7 +34,7 @@ exports[`src/components/kanban/kanbanCard return kanbanCard and click on copy li
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -147,7 +146,6 @@ exports[`src/components/kanban/kanbanCard return kanbanCard and click on delete
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -169,7 +167,7 @@ exports[`src/components/kanban/kanbanCard return kanbanCard and click on delete
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -281,7 +279,6 @@ exports[`src/components/kanban/kanbanCard return kanbanCard and click on duplica
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i
@ -303,7 +300,7 @@ exports[`src/components/kanban/kanbanCard return kanbanCard and click on duplica
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -415,7 +412,6 @@ exports[`src/components/kanban/kanbanCard should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
type="button"
>
<i

View File

@ -40,7 +40,7 @@ exports[`src/components/kanban/kanbanColumnHeader return kanbanColumnHeader and
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -85,7 +85,7 @@ exports[`src/components/kanban/kanbanColumnHeader return kanbanColumnHeader and
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -298,7 +298,7 @@ exports[`src/components/kanban/kanbanColumnHeader return kanbanColumnHeader and
</div>
</div>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -349,7 +349,7 @@ exports[`src/components/kanban/kanbanColumnHeader return kanbanColumnHeader and
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -358,7 +358,7 @@ exports[`src/components/kanban/kanbanColumnHeader return kanbanColumnHeader and
</button>
</div>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -409,7 +409,7 @@ exports[`src/components/kanban/kanbanColumnHeader should match snapshot 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -418,7 +418,7 @@ exports[`src/components/kanban/kanbanColumnHeader should match snapshot 1`] = `
</button>
</div>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i

View File

@ -30,7 +30,7 @@ exports[`components/properties/link should match snapshot for link with non-empt
</a>
<button
aria-label="Edit"
class="Button IconButton Button_Edit"
class="IconButton Button_Edit"
title="Edit"
type="button"
>
@ -40,7 +40,7 @@ exports[`components/properties/link should match snapshot for link with non-empt
</button>
<button
aria-label="Copy"
class="Button IconButton Button_Copy"
class="IconButton Button_Copy"
title="Copy"
type="button"
>
@ -67,7 +67,7 @@ exports[`components/properties/link should match snapshot for readonly link with
</a>
<button
aria-label="Copy"
class="Button IconButton Button_Copy"
class="IconButton Button_Copy"
title="Copy"
type="button"
>

View File

@ -1,132 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/sidebarSidebar global templates 1`] = `
<div>
<div
class="Sidebar octo-sidebar"
>
<div
class="octo-sidebar-header"
>
<div
class="heading"
>
<div
class="SidebarUserMenu"
>
<div
class="ModalWrapper"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="logo"
>
<div
class="logo-title"
>
<svg
class="FocalboardLogoIcon Icon"
version="1.1"
viewBox="0 0 52.589677 64"
x="0px"
y="0px"
>
<path
d="m 33.071077,12.069805 c -12.663,-3.4670001 -27.0530002,3.289 -31.6760002,16.943 -4.655,13.75 2.719,28.67 16.4690002,33.325 13.75,4.655 28.67,-2.719 33.326,-16.469 3.804,-11.235 -0.462,-22.701 -8.976,-29.249 l -0.46,4.871 h -0.001 c 4.631,4.896 6.709,11.941 4.325,18.985 -3.362,9.931 -14.447,15.151 -24.76,11.66 -10.313,-3.49 -15.9480002,-14.37 -12.5870002,-24.301 2.9750002,-8.788 11.9980002,-13.715 20.7430002,-12.625 v -10e-4 z m -6.175,16.488 c 3.456,-0.665 6.986,2.754 5.762,6.37 -0.854,2.522 -3.67,3.85 -6.291,2.962 -2.62,-0.887 -4.052,-3.651 -3.197,-6.174 0.573,-1.697 2.034,-2.852 3.726,-3.158 z m -1.285,-4.944 c -1.786,0.323 -3.45,1.104 -4.812,2.258 -1.299,1.101 -2.319,2.545 -2.898,4.258 -0.879,2.597 -0.579,5.323 0.617,7.632 1.206,2.329 3.325,4.234 6.07,5.164 2.744,0.929 5.584,0.701 7.959,-0.417 2.352,-1.107 4.246,-3.091 5.125,-5.688 0.555,-1.639 0.633,-3.254 0.344,-4.761 -0.21,-1.093 -0.615,-2.134 -1.174,-3.091 l 1.019,-5.107 c 0.189,0.187 0.374,0.378 0.552,0.574 1.75,1.919 3.008,4.283 3.508,6.877 0.415,2.154 0.304,4.457 -0.484,6.784 -1.239,3.661 -3.898,6.453 -7.193,8.005 -3.273,1.541 -7.175,1.858 -10.93,0.588 -3.754,-1.271 -6.661,-3.895 -8.326,-7.108 -1.674,-3.233 -2.09,-7.065 -0.851,-10.728 0.819,-2.419 2.26,-4.46 4.097,-6.016 1.88,-1.593 4.181,-2.673 6.656,-3.125 l -0.001,-0.004 c 1.759,-0.339 3.522,-0.313 5.213,0.016 l -3.583,3.761 c -0.294,0.028 -0.588,0.071 -0.883,0.127 h -0.025 z"
/>
<polygon
points="26.057,32.594 37.495,11.658 36.79,8.44 41.066,0.207 43.683,4.611 48.803,4.434 44.185,12.48 40.902,13.697 29.542,34.491 "
transform="translate(7.6780426e-5,-0.21919512)"
/>
</svg>
<span>
Focalboard
</span>
<div
class="versionFrame"
>
<div
class="version"
title="v0.14.0"
>
Feb 2022
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="octo-spacer"
/>
<div
class="sidebarSwitcher"
>
<button
class="Button IconButton"
type="button"
>
<svg
class="HideSidebarIcon Icon"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<polyline
points="80,20 50,50 80,80"
/>
<polyline
points="50,20 20,50, 50,80"
/>
</svg>
</button>
</div>
</div>
<div
class="octo-sidebar-list"
/>
<div
class="octo-spacer"
/>
<div
class="SidebarAddBoardMenu"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="menu-entry"
>
+ Add board
</div>
</div>
</div>
<div
class="SidebarSettingsMenu"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="menu-entry"
>
Settings
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/sidebarSidebar sidebar expect hidden 1`] = `
<div>
<div
@ -139,7 +12,7 @@ exports[`components/sidebarSidebar sidebar expect hidden 1`] = `
class="hamburger-icon"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg
@ -163,7 +36,7 @@ exports[`components/sidebarSidebar sidebar expect hidden 1`] = `
class="show-icon"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg
@ -254,7 +127,7 @@ exports[`components/sidebarSidebar sidebar hidden 1`] = `
class="sidebarSwitcher"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg
@ -514,7 +387,7 @@ exports[`components/sidebarSidebar sidebar hidden 2`] = `
class="hamburger-icon"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg
@ -538,7 +411,7 @@ exports[`components/sidebarSidebar sidebar hidden 2`] = `
class="show-icon"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg
@ -629,7 +502,7 @@ exports[`components/sidebarSidebar sidebar in dashboard page 1`] = `
class="sidebarSwitcher"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg

View File

@ -9,7 +9,7 @@ exports[`components/sidebarBoardItem sidebar call hideSidebar 1`] = `
class="octo-sidebar-item ' expanded "
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg
@ -43,7 +43,7 @@ exports[`components/sidebarBoardItem sidebar call hideSidebar 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i

View File

@ -1,103 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {useIntl, IntlShape} from 'react-intl'
import {Board} from '../../blocks/board'
import mutator from '../../mutator'
import IconButton from '../../widgets/buttons/iconButton'
import DeleteIcon from '../../widgets/icons/delete'
import EditIcon from '../../widgets/icons/edit'
import OptionsIcon from '../../widgets/icons/options'
import Menu from '../../widgets/menu'
import MenuWrapper from '../../widgets/menuWrapper'
import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../telemetry/telemetryClient'
export const addBoardFromTemplate = async (intl: IntlShape, showBoard: (id: string) => void, boardTemplateId: string, activeBoardId?: string, global = false) => {
const oldBoardId = activeBoardId
const afterRedo = async (newBoardId: string) => {
showBoard(newBoardId)
}
const beforeUndo = async () => {
if (oldBoardId) {
showBoard(oldBoardId)
}
}
const asTemplate = false
const actionDescription = intl.formatMessage({id: 'Mutator.new-board-from-template', defaultMessage: 'new board from template'})
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoardViaTemplate, {boardTemplateId})
if (global) {
await mutator.duplicateFromRootBoard(boardTemplateId, actionDescription, asTemplate, afterRedo, beforeUndo)
} else {
await mutator.duplicateBoard(boardTemplateId, actionDescription, asTemplate, afterRedo, beforeUndo)
}
}
type ButtonProps = {
showBoard: (id: string) => void
boardTemplate: Board
}
export const BoardTemplateButtonMenu = React.memo((props: ButtonProps) => {
const intl = useIntl()
const {showBoard, boardTemplate} = props
return (
<MenuWrapper stopPropagationOnToggle={true}>
<IconButton icon={<OptionsIcon/>}/>
<Menu position='right'>
<Menu.Text
icon={<EditIcon/>}
id='edit'
name={intl.formatMessage({id: 'Sidebar.edit-template', defaultMessage: 'Edit'})}
onClick={() => {
showBoard(boardTemplate.id || '')
}}
/>
<Menu.Text
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'Sidebar.delete-template', defaultMessage: 'Delete'})}
onClick={async () => {
await mutator.deleteBlock(boardTemplate, 'delete board template')
}}
/>
</Menu>
</MenuWrapper>
)
})
type Props = {
boardTemplate: Board
isGlobal: boolean
showBoard: (id: string) => void
activeBoardId?: string
}
const BoardTemplateMenuItem = React.memo((props: Props) => {
const {boardTemplate, isGlobal, activeBoardId, showBoard} = props
const intl = useIntl()
const displayName = boardTemplate.title || intl.formatMessage({id: 'Sidebar.untitled', defaultMessage: 'Untitled'})
return (
<Menu.Text
key={boardTemplate.id || ''}
id={boardTemplate.id || ''}
name={displayName}
icon={<div className='Icon'>{boardTemplate.fields.icon}</div>}
onClick={() => {
addBoardFromTemplate(intl, showBoard, boardTemplate.id || '', activeBoardId, isGlobal)
}}
rightIcon={!isGlobal &&
<BoardTemplateButtonMenu
boardTemplate={boardTemplate}
showBoard={showBoard}
/>
}
/>
)
})
export default BoardTemplateMenuItem

View File

@ -15,6 +15,7 @@ type Props = {
boardTitle: string;
onClose: () => void;
onDelete: () => Promise<void>
isTemplate?: boolean;
}
export default function DeleteBoardDialog(props: Props): JSX.Element {
@ -29,25 +30,43 @@ export default function DeleteBoardDialog(props: Props): JSX.Element {
>
<div className='container'>
<h2 className='header text-heading5'>
<FormattedMessage
id='DeleteBoardDialog.confirm-tite'
defaultMessage='Confirm Delete Board'
/>
{props.isTemplate &&
<FormattedMessage
id='DeleteBoardDialog.confirm-tite-template'
defaultMessage='Confirm Delete Board Template'
/>}
{!props.isTemplate &&
<FormattedMessage
id='DeleteBoardDialog.confirm-tite'
defaultMessage='Confirm Delete Board'
/>}
</h2>
<p className='body'>
<FormattedMessage
id='DeleteBoardDialog.confirm-info'
defaultMessage='Are you sure you want to delete the board “{boardTitle}”? Deleting it will delete all cards in the board.'
values={{
boardTitle: props.boardTitle,
}}
/>
{props.isTemplate &&
<FormattedMessage
id='DeleteBoardDialog.confirm-info'
defaultMessage='Are you sure you want to delete the board template “{boardTitle}”?'
values={{
boardTitle: props.boardTitle,
}}
/>}
{!props.isTemplate &&
<FormattedMessage
id='DeleteBoardDialog.confirm-info'
defaultMessage='Are you sure you want to delete the board “{boardTitle}”? Deleting it will delete all cards in the board.'
values={{
boardTitle: props.boardTitle,
}}
/>}
</p>
<div className='footer'>
<Button
size={'medium'}
emphasis={'tertiary'}
onClick={() => !isSubmitting && props.onClose()}
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
!isSubmitting && props.onClose()
}}
>
<FormattedMessage
id='DeleteBoardDialog.confirm-cancel'
@ -58,15 +77,16 @@ export default function DeleteBoardDialog(props: Props): JSX.Element {
size={'medium'}
filled={true}
danger={true}
onClick={async () => {
onClick={async (e: React.MouseEvent) => {
e.stopPropagation()
try {
setSubmitting(true)
await props.onDelete()
setSubmitting(false)
props.onClose()
} catch (e) {
} catch (err) {
setSubmitting(false)
Utils.logError(`Delete board ERROR: ${e}`)
Utils.logError(`Delete board ERROR: ${err}`)
// TODO: display error on screen
}

View File

@ -137,6 +137,19 @@
stroke-width: 6px;
}
.add-board {
display: flex;
padding: 0 16px 0 20px;
cursor: pointer;
color: rgba(var(--sidebar-text-rgb), 0.64);
height: 36px;
align-items: center;
&:hover {
background-color: rgba(var(--sidebar-text-rgb), 0.08);
}
}
@media screen and (min-width: 768px) {
.WorkspaceTitle .sidebarSwitcher {
display: none;

View File

@ -97,13 +97,13 @@ describe('components/sidebarSidebar', () => {
const {container} = render(component)
expect(container).toMatchSnapshot()
const hideSidebar = container.querySelector('.Button > .HideSidebarIcon')
const hideSidebar = container.querySelector('button > .HideSidebarIcon')
expect(hideSidebar).toBeDefined()
userEvent.click(hideSidebar as Element)
expect(container).toMatchSnapshot()
const showSidebar = container.querySelector('.Button > .ShowSidebarIcon')
const showSidebar = container.querySelector('button > .ShowSidebarIcon')
expect(showSidebar).toBeDefined()
})
@ -139,62 +139,12 @@ describe('components/sidebarSidebar', () => {
const {container} = render(component)
expect(container).toMatchSnapshot()
const hideSidebar = container.querySelector('.Button > .HideSidebarIcon')
const hideSidebar = container.querySelector('button > .HideSidebarIcon')
expect(hideSidebar).toBeNull()
const showSidebar = container.querySelector('.Button > .ShowSidebarIcon')
const showSidebar = container.querySelector('button > .ShowSidebarIcon')
expect(showSidebar).toBeDefined()
customGlobal.innerWidth = 1024
})
test('global templates', () => {
const store = mockStore({
workspace: {
userWorkspaces: new Array<UserWorkspace>(workspace1, workspace2, workspace3),
},
boards: {
boards: [],
templates: [
{id: '1', title: 'Template 1', fields: {icon: '🚴🏻‍♂️'}},
{id: '2', title: 'Template 2', fields: {icon: '🚴🏻‍♂️'}},
{id: '3', title: 'Template 3', fields: {icon: '🚴🏻‍♂️'}},
{id: '4', title: 'Template 4', fields: {icon: '🚴🏻‍♂️'}},
],
},
views: {
views: [],
},
users: {
me: {},
},
globalTemplates: {
value: [],
},
})
const history = createMemoryHistory()
const component = wrapIntl(
<ReduxProvider store={store}>
<Router history={history}>
<Sidebar/>
</Router>
</ReduxProvider>,
)
const {container} = render(component)
expect(container).toMatchSnapshot()
const addBoardButton = container.querySelector('.SidebarAddBoardMenu > .MenuWrapper')
expect(addBoardButton).toBeDefined()
userEvent.click(addBoardButton as Element)
const templates = container.querySelectorAll('.SidebarAddBoardMenu > .MenuWrapper div:not(.hideOnWidescreen).menu-options .menu-name')
expect(templates).toBeDefined()
console.log(templates[0].innerHTML)
console.log(templates[1].innerHTML)
// 4 mocked templates, one "Select a template", one "Empty Board" and one "+ New Template"
expect(templates.length).toBe(7)
})
})

View File

@ -1,8 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react'
import {useIntl} from 'react-intl'
import {FormattedMessage, useIntl} from 'react-intl'
import DashboardOnboardingSvg from '../../svg/dashboard-onboarding'
@ -21,7 +20,6 @@ import './sidebar.scss'
import WorkspaceSwitcher from '../workspaceSwitcher/workspaceSwitcher'
import SidebarAddBoardMenu from './sidebarAddBoardMenu'
import SidebarBoardItem from './sidebarBoardItem'
import SidebarSettingsMenu from './sidebarSettingsMenu'
import SidebarUserMenu from './sidebarUserMenu'
@ -30,6 +28,7 @@ type Props = {
activeBoardId?: string
activeViewId?: string
isDashboard?: boolean
onBoardTemplateSelectorOpen?: () => void
}
function getWindowDimensions() {
@ -148,14 +147,17 @@ const Sidebar = React.memo((props: Props) => {
{
workspace && workspace.id !== '0' && !props.isDashboard &&
<WorkspaceSwitcher activeWorkspace={workspace}/>
<WorkspaceSwitcher
activeWorkspace={workspace}
onBoardTemplateSelectorOpen={props.onBoardTemplateSelectorOpen}
/>
}
{
props.isDashboard &&
(
<React.Fragment>
<WorkspaceSwitcher/>
<WorkspaceSwitcher onBoardTemplateSelectorOpen={props.onBoardTemplateSelectorOpen}/>
<div className='Sidebar__onboarding'>
<DashboardOnboardingSvg/>
<div>
@ -192,9 +194,15 @@ const Sidebar = React.memo((props: Props) => {
{
(!props.isDashboard && !Utils.isFocalboardPlugin()) &&
<SidebarAddBoardMenu
activeBoardId={props.activeBoardId}
/>
<div
className='add-board'
onClick={props.onBoardTemplateSelectorOpen}
>
<FormattedMessage
id='Sidebar.add-board'
defaultMessage='+ Add board'
/>
</div>
}
{!Utils.isFocalboardPlugin() &&

View File

@ -1,18 +0,0 @@
.SidebarAddBoardMenu {
.menu-entry {
display: flex;
cursor: pointer;
flex-direction: row;
padding: 0 16px 0 8px;
height: 36px;
align-items: center;
padding-left: 20px;
color: rgba(var(--sidebar-text-rgb), 0.64);
font-weight: 400;
&:hover {
background-color: rgba(var(--sidebar-text-rgb), 0.08);
display: flex;
}
}
}

View File

@ -1,171 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect} from 'react'
import {FormattedMessage, IntlShape, useIntl} from 'react-intl'
import {generatePath, useHistory, useRouteMatch} from 'react-router-dom'
import {Block} from '../../blocks/block'
import {Board, createBoard} from '../../blocks/board'
import {createBoardView} from '../../blocks/boardView'
import mutator from '../../mutator'
import octoClient from '../../octoClient'
import {getSortedTemplates} from '../../store/boards'
import {fetchGlobalTemplates, getGlobalTemplates} from '../../store/globalTemplates'
import {useAppDispatch, useAppSelector} from '../../store/hooks'
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
import AddIcon from '../../widgets/icons/add'
import BoardIcon from '../../widgets/icons/board'
import Menu from '../../widgets/menu'
import MenuWrapper from '../../widgets/menuWrapper'
import BoardTemplateMenuItem from './boardTemplateMenuItem'
import './sidebarAddBoardMenu.scss'
type Props = {
activeBoardId?: string
}
export const addBoardClicked = async (showBoard: (id: string) => void, intl: IntlShape, activeBoardId?: string) => {
const oldBoardId = activeBoardId
const board = createBoard()
board.rootId = board.id
const view = createBoardView()
view.fields.viewType = 'board'
view.parentId = board.id
view.rootId = board.rootId
view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board view'})
await mutator.insertBlocks(
[board, view],
'add board',
async (newBlocks: Block[]) => {
const newBoardId = newBlocks[0].id
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoard, {board: newBoardId})
showBoard(newBoardId)
},
async () => {
if (oldBoardId) {
showBoard(oldBoardId)
}
},
)
}
export const addBoardTemplateClicked = async (showBoard: (id: string) => void, intl: IntlShape, activeBoardId?: string) => {
const boardTemplate = createBoard()
boardTemplate.rootId = boardTemplate.id
boardTemplate.fields.isTemplate = true
boardTemplate.title = intl.formatMessage({id: 'View.NewTemplateTitle', defaultMessage: 'Untitled Template'})
const view = createBoardView()
view.fields.viewType = 'board'
view.parentId = boardTemplate.id
view.rootId = boardTemplate.rootId
view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board view'})
await mutator.insertBlocks(
[boardTemplate, view],
'add board template',
async (newBlocks: Block[]) => {
const newBoardId = newBlocks[0].id
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoardTemplate, {board: newBoardId})
showBoard(newBoardId)
}, async () => {
if (activeBoardId) {
showBoard(activeBoardId)
}
},
)
}
const SidebarAddBoardMenu = (props: Props): JSX.Element => {
const globalTemplates = useAppSelector<Board[]>(getGlobalTemplates)
const dispatch = useAppDispatch()
const history = useHistory()
const match = useRouteMatch<{boardId: string, viewId?: string}>()
const showBoard = useCallback((boardId) => {
const params = {...match.params, boardId: boardId || ''}
delete params.viewId
const newPath = generatePath(match.path, params)
history.push(newPath)
}, [match, history])
useEffect(() => {
if (octoClient.workspaceId !== '0' && globalTemplates.length === 0) {
dispatch(fetchGlobalTemplates())
}
}, [octoClient.workspaceId])
const intl = useIntl()
const templates = useAppSelector(getSortedTemplates)
if (!templates) {
return <div/>
}
return (
<div className='SidebarAddBoardMenu'>
<MenuWrapper>
<div className='menu-entry'>
<FormattedMessage
id='Sidebar.add-board'
defaultMessage='+ Add board'
/>
</div>
<Menu position='top'>
{templates.length > 0 && <>
<Menu.Label>
<b>
<FormattedMessage
id='Sidebar.select-a-template'
defaultMessage='Select a template'
/>
</b>
</Menu.Label>
<Menu.Separator/>
</>}
{templates.map((boardTemplate) => (
<BoardTemplateMenuItem
key={boardTemplate.id}
boardTemplate={boardTemplate}
isGlobal={false}
showBoard={showBoard}
activeBoardId={props.activeBoardId}
/>
))}
{globalTemplates.map((boardTemplate: Board) => (
<BoardTemplateMenuItem
key={boardTemplate.id}
boardTemplate={boardTemplate}
isGlobal={true}
showBoard={showBoard}
activeBoardId={props.activeBoardId}
/>
))}
<Menu.Text
id='empty-template'
name={intl.formatMessage({id: 'Sidebar.empty-board', defaultMessage: 'Empty board'})}
icon={<BoardIcon/>}
onClick={() => addBoardClicked(showBoard, intl, props.activeBoardId)}
/>
<Menu.Text
icon={<AddIcon/>}
id='add-template'
name={intl.formatMessage({id: 'Sidebar.add-template', defaultMessage: 'New template'})}
onClick={() => addBoardTemplateClicked(showBoard, intl, props.activeBoardId)}
/>
</Menu>
</MenuWrapper>
</div>
)
}
export default SidebarAddBoardMenu

View File

@ -1928,7 +1928,7 @@ exports[`components/table/Table should match snapshot with GroupBy 1`] = `
style="width: 100px;"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg
@ -1963,7 +1963,7 @@ exports[`components/table/Table should match snapshot with GroupBy 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -1972,7 +1972,7 @@ exports[`components/table/Table should match snapshot with GroupBy 1`] = `
</button>
</div>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i

View File

@ -12,7 +12,7 @@ exports[`should match snapshot on read only 1`] = `
style="width: 100px;"
>
<button
class="Button IconButton readonly"
class="IconButton readonly"
type="button"
>
<svg
@ -62,7 +62,7 @@ exports[`should match snapshot with Group 1`] = `
style="width: 100px;"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg
@ -101,7 +101,7 @@ exports[`should match snapshot with Group 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -110,7 +110,7 @@ exports[`should match snapshot with Group 1`] = `
</button>
</div>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -133,7 +133,7 @@ exports[`should match snapshot, add new 1`] = `
style="width: 100px;"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg
@ -172,7 +172,7 @@ exports[`should match snapshot, add new 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -181,7 +181,7 @@ exports[`should match snapshot, add new 1`] = `
</button>
</div>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -204,7 +204,7 @@ exports[`should match snapshot, edit title 1`] = `
style="width: 100px;"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg
@ -243,7 +243,7 @@ exports[`should match snapshot, edit title 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -252,7 +252,7 @@ exports[`should match snapshot, edit title 1`] = `
</button>
</div>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -275,7 +275,7 @@ exports[`should match snapshot, hide group 1`] = `
style="width: 100px;"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg
@ -314,7 +314,7 @@ exports[`should match snapshot, hide group 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -323,7 +323,7 @@ exports[`should match snapshot, hide group 1`] = `
</button>
</div>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -346,7 +346,7 @@ exports[`should match snapshot, no groups 1`] = `
style="width: 100px;"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg
@ -381,7 +381,7 @@ exports[`should match snapshot, no groups 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -390,7 +390,7 @@ exports[`should match snapshot, no groups 1`] = `
</button>
</div>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i

View File

@ -32,7 +32,7 @@ exports[`components/viewHeader/emptyCardButton return EmptyCardButton 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -76,7 +76,7 @@ exports[`components/viewHeader/emptyCardButton return EmptyCardButton and Set Te
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -179,7 +179,7 @@ exports[`components/viewHeader/emptyCardButton return EmptyCardButton and addCar
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i

View File

@ -10,7 +10,7 @@ exports[`components/viewHeader/filterComponent return filterComponent 1`] = `
>
<button
aria-label="Close"
class="Button IconButton"
class="IconButton"
title="Close"
type="button"
>
@ -193,7 +193,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and add Fi
>
<button
aria-label="Close"
class="Button IconButton"
class="IconButton"
title="Close"
type="button"
>
@ -376,7 +376,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and click
>
<button
aria-label="Close"
class="Button IconButton"
class="IconButton"
title="Close"
type="button"
>
@ -559,7 +559,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and filter
>
<button
aria-label="Close"
class="Button IconButton"
class="IconButton"
title="Close"
type="button"
>

View File

@ -61,7 +61,7 @@ exports[`components/viewHeader/newCardButton return NewCardButton 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -180,7 +180,7 @@ exports[`components/viewHeader/newCardButton return NewCardButton and addCard 1`
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -299,7 +299,7 @@ exports[`components/viewHeader/newCardButton return NewCardButton and addCardTem
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i

View File

@ -23,7 +23,7 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -85,7 +85,7 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -151,7 +151,7 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -213,7 +213,7 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -279,7 +279,7 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -314,7 +314,7 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -376,7 +376,7 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"
@ -442,7 +442,7 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -504,7 +504,7 @@ exports[`components/viewHeader/newCardButtonTemplateItem return NewCardButtonTem
role="button"
>
<i
class="CompassIcon icon-trash-can-outline trash-can-outline"
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
<div
class="menu-name"

View File

@ -18,7 +18,7 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -105,7 +105,7 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -159,7 +159,7 @@ exports[`components/viewHeader/viewHeader return viewHeader readonly 1`] = `
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i

View File

@ -11,7 +11,7 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -121,7 +121,7 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -231,7 +231,7 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu with Share Boar
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i
@ -341,7 +341,7 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu without Share B
role="button"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<i

View File

@ -6,6 +6,7 @@
position: relative;
> .mainFrame {
position: relative;
flex: 1 1 auto;
display: flex;
flex-direction: column;

View File

@ -210,7 +210,7 @@ describe('src/components/workspace', () => {
expect(container).toMatchSnapshot()
expect(mockedUtils.getReadToken).toBeCalledTimes(1)
})
test('return workspace with EmptyCenterPanel component', async () => {
test('return workspace with BoardTemplateSelector component', async () => {
const emptyStore = mockStateStore([], {
users: {
me,

View File

@ -1,9 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect} from 'react'
import React, {useCallback, useEffect, useState} from 'react'
import {generatePath, useRouteMatch, useHistory} from 'react-router-dom'
import {FormattedMessage} from 'react-intl'
import {getCurrentWorkspace} from '../store/workspace'
import {getCurrentBoard} from '../store/boards'
import {getCurrentViewCardsSortedFilteredAndGrouped} from '../store/cards'
import {getView, getCurrentBoardViews, getCurrentViewGroupBy, getCurrentView, getCurrentViewDisplayBy} from '../store/views'
@ -16,7 +17,7 @@ import {ClientConfig} from '../config/clientConfig'
import {Utils} from '../utils'
import CenterPanel from './centerPanel'
import EmptyCenterPanel from './emptyCenterPanel'
import BoardTemplateSelector from './boardTemplateSelector/boardTemplateSelector'
import Sidebar from './sidebar/sidebar'
import './workspace.scss'
@ -26,6 +27,7 @@ type Props = {
}
function CenterContent(props: Props) {
const workspace = useAppSelector(getCurrentWorkspace)
const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string}>()
const board = useAppSelector(getCurrentBoard)
const cards = useAppSelector(getCurrentViewCardsSortedFilteredAndGrouped)
@ -85,23 +87,55 @@ function CenterContent(props: Props) {
}
return (
<EmptyCenterPanel/>
<BoardTemplateSelector
title={
<FormattedMessage
id='BoardTemplateSelector.plugin.no-content-title'
defaultMessage='Create a Board in {workspaceName}'
values={{workspaceName: workspace?.title}}
/>
}
description={
<FormattedMessage
id='BoardTemplateSelector.plugin.no-content-description'
defaultMessage='Add a board to the sidebar using any of the templates defined below or start from scratch.{lineBreak} Members of "{workspaceName}" will have access to boards created here.'
values={{
workspaceName: <b>{workspace?.title}</b>,
lineBreak: <br/>,
}}
/>
}
/>
)
}
const Workspace = React.memo((props: Props) => {
const board = useAppSelector(getCurrentBoard)
const view = useAppSelector(getCurrentView)
const [boardTemplateSelectorOpen, setBoardTemplateSelectorOpen] = useState(false)
const closeBoardTemplateSelector = useCallback(() => {
setBoardTemplateSelectorOpen(false)
}, [])
const openBoardTemplateSelector = useCallback(() => {
setBoardTemplateSelectorOpen(true)
}, [])
useEffect(() => {
setBoardTemplateSelectorOpen(false)
}, [board, view])
return (
<div className='Workspace'>
{!props.readonly &&
<Sidebar
onBoardTemplateSelectorOpen={openBoardTemplateSelector}
activeBoardId={board?.id}
activeViewId={view?.id}
/>
}
<div className='mainFrame'>
{boardTemplateSelectorOpen &&
<BoardTemplateSelector onClose={closeBoardTemplateSelector}/>}
{(board?.fields.isTemplate) &&
<div className='banner'>
<FormattedMessage

View File

@ -10,6 +10,7 @@
padding: 0 16px;
margin-bottom: 16px;
font-weight: 600;
align-items: center;
> div:nth-child(2) {
position: absolute;
@ -21,19 +22,6 @@
border-radius: 4px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.add-workspace-icon {
display: flex;
i {
color: rgb(var(--sidebar-text-rgb));
align-self: center;
}
&:hover {
cursor: pointer;
}
}
}

View File

@ -8,9 +8,7 @@ import {useHistory} from 'react-router-dom'
import {IWorkspace} from '../../blocks/workspace'
import ChevronDown from '../../widgets/icons/chevronDown'
import AddIcon from '../../widgets/icons/add'
import {setCurrent as setCurrentBoard} from '../../store/boards'
import {setCurrent as setCurrentView} from '../../store/views'
import {useAppDispatch} from '../../store/hooks'
import IconButton from '../../widgets/buttons/iconButton'
import {UserSettings} from '../../userSettings'
@ -18,22 +16,14 @@ import WorkspaceOptions, {DashboardOption} from './workspaceOptions'
type Props = {
activeWorkspace?: IWorkspace
onBoardTemplateSelectorOpen?: () => void
}
const WorkspaceSwitcher = (props: Props): JSX.Element => {
const history = useHistory()
const {activeWorkspace} = props
const dispatch = useAppDispatch()
const [showMenu, setShowMenu] = useState<boolean>(false)
const goToEmptyCenterPanel = () => {
UserSettings.lastBoardId = null
UserSettings.lastViewId = null
dispatch(setCurrentBoard(''))
dispatch(setCurrentView(''))
history.replace(`/workspace/${activeWorkspace?.id}`)
}
return (
<div className={'WorkspaceSwitcherWrapper'}>
<div
@ -69,12 +59,12 @@ const WorkspaceSwitcher = (props: Props): JSX.Element => {
/>
}
{activeWorkspace &&
<span
className='add-workspace-icon'
onClick={goToEmptyCenterPanel}
>
<AddIcon/>
</span>
<IconButton
onClick={props.onBoardTemplateSelectorOpen}
icon={<AddIcon/>}
inverted={true}
size='small'
/>
}
</div>
)

View File

@ -1,10 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IntlShape} from 'react-intl'
import {batch} from 'react-redux'
import {BlockIcons} from './blockIcons'
import {Block, BlockPatch, createPatchesFromBlocks} from './blocks/block'
import {Board, IPropertyOption, IPropertyTemplate, PropertyType, createBoard} from './blocks/board'
import {BoardView, ISortOption, createBoardView, KanbanCalculationFields} from './blocks/boardView'
import {Card, createCard} from './blocks/card'
import {ContentBlock} from './blocks/contentBlock'
import {CommentBlock} from './blocks/commentBlock'
import {FilterGroup} from './blocks/filterGroup'
import octoClient, {OctoClient} from './octoClient'
import {OctoUtils} from './octoUtils'
@ -12,6 +18,22 @@ import undoManager from './undomanager'
import {Utils, IDType} from './utils'
import {UserSettings} from './userSettings'
import TelemetryClient, {TelemetryCategory, TelemetryActions} from './telemetry/telemetryClient'
import store from './store'
import {updateBoards} from './store/boards'
import {updateViews} from './store/views'
import {updateCards} from './store/cards'
import {updateComments} from './store/comments'
import {updateContents} from './store/contents'
function updateAllBlocks(blocks: Block[]) {
return batch(() => {
store.dispatch(updateBoards(blocks.filter((b: Block) => b.type === '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(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[]))
})
}
//
// The Mutator is used to make all changes to server state
@ -115,6 +137,7 @@ class Mutator {
async () => {
const res = await octoClient.insertBlocks(blocks)
const newBlocks = (await res.json()) as Block[]
updateAllBlocks(newBlocks)
await afterRedo?.(newBlocks)
return newBlocks
},
@ -766,11 +789,13 @@ class Mutator {
newBlocks,
description,
async (respBlocks: Block[]) => {
await afterRedo?.(respBlocks[0].id)
const board = respBlocks.find((b) => b.type === 'board')
await afterRedo?.(board?.id || '')
},
beforeUndo,
)
return [createdBlocks, createdBlocks[0].id]
const board = createdBlocks.find((b: Block) => b.type === 'board')
return [createdBlocks, board.id]
}
async duplicateFromRootBoard(
@ -799,11 +824,84 @@ class Mutator {
newBlocks,
description,
async (respBlocks: Block[]) => {
await afterRedo?.(respBlocks[0].id)
const board = respBlocks.find((b) => b.type === 'board')
await afterRedo?.(board?.id || '')
},
beforeUndo,
)
const board = createdBlocks.find((b: Block) => b.type === 'board')
return [createdBlocks, board.id]
}
async addBoardFromTemplate(
intl: IntlShape,
afterRedo: (id: string) => Promise<void>,
beforeUndo: () => Promise<void>,
boardTemplateId: string,
global = false,
): Promise<[Block[], string]> {
const asTemplate = false
const actionDescription = intl.formatMessage({id: 'Mutator.new-board-from-template', defaultMessage: 'new board from template'})
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoardViaTemplate, {boardTemplateId})
if (global) {
return mutator.duplicateFromRootBoard(boardTemplateId, actionDescription, asTemplate, afterRedo, beforeUndo)
}
return mutator.duplicateBoard(boardTemplateId, actionDescription, asTemplate, afterRedo, beforeUndo)
}
async addEmptyBoard(
intl: IntlShape,
afterRedo: (id: string) => Promise<void>,
beforeUndo: () => Promise<void>,
): Promise<Block[]> {
const board = createBoard()
board.rootId = board.id
const view = createBoardView()
view.fields.viewType = 'board'
view.parentId = board.id
view.rootId = board.rootId
view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board view'})
return mutator.insertBlocks(
[board, view],
'add board',
async (newBlocks: Block[]) => {
const newBoard = newBlocks.find((b) => b.type === 'board')
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoard, {board: newBoard?.id})
await afterRedo(newBoard?.id || '')
},
beforeUndo,
)
}
async addEmptyBoardTemplate(
intl: IntlShape,
afterRedo: (id: string) => Promise<void>,
beforeUndo: () => Promise<void>,
): Promise<Block[]> {
const boardTemplate = createBoard()
boardTemplate.rootId = boardTemplate.id
boardTemplate.fields.isTemplate = true
boardTemplate.title = intl.formatMessage({id: 'View.NewTemplateTitle', defaultMessage: 'Untitled Template'})
const view = createBoardView()
view.fields.viewType = 'board'
view.parentId = boardTemplate.id
view.rootId = boardTemplate.rootId
view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board view'})
return mutator.insertBlocks(
[boardTemplate, view],
'add board template',
async (newBlocks: Block[]) => {
const newBoard = newBlocks.find((b) => b.type === 'board')
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoardTemplate, {board: newBoard?.id})
afterRedo(newBoard?.id || '')
},
beforeUndo,
)
return [createdBlocks, createdBlocks[0].id]
}
// Other methods

View File

@ -1,4 +1,6 @@
.BoardPage {
position: relative;
> .error {
background-color: rgba(230, 192, 192, 0.9);
text-align: center;

View File

@ -72,7 +72,7 @@ exports[`pages/dashboard/DashboardPage base case 1`] = `
class="sidebarSwitcher"
>
<button
class="Button IconButton"
class="IconButton"
type="button"
>
<svg

View File

@ -66,6 +66,21 @@ export const getSortedViews = createSelector(
},
)
export const getViewsByBoard = createSelector(
getViews,
(views) => {
const result: {[key: string]: BoardView[]} = {}
Object.values(views).forEach((view) => {
if (result[view.parentId]) {
result[view.parentId].push(view)
} else {
result[view.parentId] = [view]
}
})
return result
},
)
export function getView(viewId: string): (state: RootState) => BoardView|null {
return (state: RootState): BoardView|null => {
return state.views.views[viewId] || null

View File

@ -13,6 +13,7 @@ export const TelemetryActions = {
CreateBoard: 'createBoard',
DuplicateBoard: 'duplicateBoard',
DeleteBoard: 'deleteBoard',
DeleteBoardTemplate: 'deleteBoardTemplate',
ShareBoard: 'shareBoard',
CreateBoardTemplate: 'createBoardTemplate',
CreateBoardViaTemplate: 'createBoardViaTemplate',

View File

@ -1,6 +1,5 @@
.Button {
--danger-button-bg-rgb: 247, 67, 67;
font-family: 'Open Sans', sans-serif;
display: flex;
flex: 0 0 auto;
@ -13,7 +12,7 @@
overflow: hidden;
background: transparent;
border: 0;
transition: background 100ms ease-out 0s;
transition: all 100ms ease-out 0s;
color: inherit;
height: 32px;
padding: 0 10px;

View File

@ -1,8 +1,40 @@
.IconButton {
border-radius: 4px;
height: 24px;
width: 24px;
padding: 0;
margin: 0;
background: transparent;
border: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 100ms ease-out 0s;
color: rgba(var(--center-channel-color-rgb), 0.56);
&:hover {
color: rgba(var(--center-channel-color-rgb), 0.72);
background-color: rgba(var(--center-channel-color-rgb), 0.08);
}
&:active {
background-color: rgba(var(--button-bg-rgb), 0.08);
color: rgba(var(--button-bg-rgb), 1);
}
&.style--inverted {
color: rgba(var(--sidebar-text-rgb), 0.64);
&:hover {
color: rgba(var(--sidebar-text-rgb), 1);
background-color: rgba(var(--button-bg-rgb), 0.08);
}
&:active {
color: rgba(var(--sidebar-text-rgb), 1);
background-color: rgba(var(--button-bg-rgb), 0.16);
}
}
.Icon {
height: 24px;
@ -11,14 +43,22 @@
margin: 0;
}
&--large {
&.size--large {
width: 48px;
height: 48px;
font-size: 31.2px;
}
&.size--medium {
width: 40px;
height: 40px;
font-size: 24px;
}
.Icon {
width: 32px;
height: 32px;
}
&.size--small {
width: 32px;
height: 32px;
font-size: 18px;
}
&.margin-left {

View File

@ -3,26 +3,32 @@
import React from 'react'
import './iconButton.scss'
import {Utils} from '../../utils'
type Props = {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
title?: string
icon?: React.ReactNode
className?: string
size?: string
inverted?: boolean
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement>) => void
}
function IconButton(props: Props): JSX.Element {
let className = 'Button IconButton'
if (props.className) {
className += ' ' + props.className
const classNames: Record<string, boolean> = {
IconButton: true,
'style--inverted': Boolean(props.inverted),
}
classNames[`${props.className}`] = Boolean(props.className)
classNames[`size--${props.size}`] = Boolean(props.size)
return (
<button
type='button'
onClick={props.onClick}
onMouseDown={props.onMouseDown}
className={className}
className={Utils.generateClassName(classNames)}
title={props.title}
aria-label={props.title}
>

View File

@ -1,4 +0,0 @@
.CloseIcon {
color: rgb(var(--center-channel-color-rgb), 0.5);
font-size: 24px;
}

View File

@ -5,8 +5,6 @@ import React from 'react'
import CompassIcon from './compassIcon'
import './close.scss'
export default function CloseIcon(): JSX.Element {
return (
<CompassIcon

View File

@ -11,7 +11,7 @@ export default function DeleteIcon(): JSX.Element {
return (
<CompassIcon
icon='trash-can-outline'
className='trash-can-outline'
className='DeleteIcon trash-can-outline'
/>
)
}

View File

@ -1,7 +0,0 @@
.EditIcon {
fill: rgba(var(--center-channel-color-rgb), 0.5);
stroke: none;
width: 24px;
height: 24px;
font-size: 18px;
}

View File

@ -3,7 +3,6 @@
import React from 'react'
import './edit.scss'
import CompassIcon from './compassIcon'
export default function EditIcon(): JSX.Element {