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:
parent
65c783c270
commit
dcf7600ca4
1
webapp/cypress/global.d.ts
vendored
1
webapp/cypress/global.d.ts
vendored
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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**')
|
||||
|
@ -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')
|
||||
|
@ -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}`)
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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
56
webapp/src/boardUtils.ts
Normal 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}
|
||||
}
|
@ -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"
|
||||
|
@ -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
@ -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"
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
`;
|
@ -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"
|
||||
>
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
`;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
})
|
||||
})
|
||||
})
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
})
|
||||
})
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
@ -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()
|
||||
})
|
||||
})
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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))
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -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
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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() &&
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -6,6 +6,7 @@
|
||||
position: relative;
|
||||
|
||||
> .mainFrame {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,6 @@
|
||||
.BoardPage {
|
||||
position: relative;
|
||||
|
||||
> .error {
|
||||
background-color: rgba(230, 192, 192, 0.9);
|
||||
text-align: center;
|
||||
|
@ -72,7 +72,7 @@ exports[`pages/dashboard/DashboardPage base case 1`] = `
|
||||
class="sidebarSwitcher"
|
||||
>
|
||||
<button
|
||||
class="Button IconButton"
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
|
@ -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
|
||||
|
@ -13,6 +13,7 @@ export const TelemetryActions = {
|
||||
CreateBoard: 'createBoard',
|
||||
DuplicateBoard: 'duplicateBoard',
|
||||
DeleteBoard: 'deleteBoard',
|
||||
DeleteBoardTemplate: 'deleteBoardTemplate',
|
||||
ShareBoard: 'shareBoard',
|
||||
CreateBoardTemplate: 'createBoardTemplate',
|
||||
CreateBoardViaTemplate: 'createBoardViaTemplate',
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -1,4 +0,0 @@
|
||||
.CloseIcon {
|
||||
color: rgb(var(--center-channel-color-rgb), 0.5);
|
||||
font-size: 24px;
|
||||
}
|
@ -5,8 +5,6 @@ import React from 'react'
|
||||
|
||||
import CompassIcon from './compassIcon'
|
||||
|
||||
import './close.scss'
|
||||
|
||||
export default function CloseIcon(): JSX.Element {
|
||||
return (
|
||||
<CompassIcon
|
||||
|
@ -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'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
.EditIcon {
|
||||
fill: rgba(var(--center-channel-color-rgb), 0.5);
|
||||
stroke: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 18px;
|
||||
}
|
@ -3,7 +3,6 @@
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import './edit.scss'
|
||||
import CompassIcon from './compassIcon'
|
||||
|
||||
export default function EditIcon(): JSX.Element {
|
||||
|
Loading…
x
Reference in New Issue
Block a user