From 7649ef2208e5b64e3a300c2933afb071522b4104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 17 Nov 2020 18:53:29 +0100 Subject: [PATCH 01/23] make the board list of the sidebar scrollable --- webapp/src/components/sidebar.scss | 7 +- webapp/src/components/sidebar.tsx | 192 +++++++++++++++-------------- 2 files changed, 103 insertions(+), 96 deletions(-) diff --git a/webapp/src/components/sidebar.scss b/webapp/src/components/sidebar.scss index 2e6490ad1..36c2be062 100644 --- a/webapp/src/components/sidebar.scss +++ b/webapp/src/components/sidebar.scss @@ -8,7 +8,6 @@ color: rgb(var(--sidebar-fg)); background-color: rgb(var(--sidebar-bg)); padding: 10px 0; - overflow-y: scroll; &.hidden { position: absolute; @@ -30,6 +29,12 @@ flex-shrink: 0; } + .octo-sidebar-list { + overflow-y: auto; + max-height: calc(100% - 78px); + max-width: 250px; + } + .octo-sidebar-header { display: flex; flex-direction: row; diff --git a/webapp/src/components/sidebar.tsx b/webapp/src/components/sidebar.tsx index 24a82bc85..193edc5dc 100644 --- a/webapp/src/components/sidebar.tsx +++ b/webapp/src/components/sidebar.tsx @@ -87,111 +87,113 @@ class Sidebar extends React.Component { icon={} /> - { - boards.map((board) => { - const displayTitle: string = board.title || intl.formatMessage({id: 'Sidebar.untitled-board', defaultMessage: '(Untitled Board)'}) - const boardViews = views.filter((view) => view.parentId === board.id) - return ( -
-
- } - onClick={() => { - const newCollapsedBoards = {...this.state.collapsedBoards} - newCollapsedBoards[board.id] = !newCollapsedBoards[board.id] - this.setState({collapsedBoards: newCollapsedBoards}) - }} - /> -
{ - this.boardClicked(board) - }} - title={displayTitle} - > - {board.icon ? `${board.icon} ${displayTitle}` : displayTitle} -
- - }/> - - } - onClick={async () => { - const nextBoardId = boards.length > 1 ? boards.find((o) => o.id !== board.id)?.id : undefined - mutator.deleteBlock( - board, - 'delete block', - async () => { - nextBoardId && this.props.showBoard(nextBoardId!) - }, - async () => { - this.props.showBoard(board.id) - }, - ) - }} - /> - - } - onClick={async () => { - await mutator.duplicateBoard( - board.id, - 'duplicate board', - async (newBoardId) => { - newBoardId && this.props.showBoard(newBoardId) - }, - async () => { - this.props.showBoard(board.id) - }, - ) - }} - /> - - -
- {!collapsedBoards[board.id] && boardViews.length === 0 && -
- + { + boards.map((board) => { + const displayTitle: string = board.title || intl.formatMessage({id: 'Sidebar.untitled-board', defaultMessage: '(Untitled Board)'}) + const boardViews = views.filter((view) => view.parentId === board.id) + return ( +
+
+ } + onClick={() => { + const newCollapsedBoards = {...this.state.collapsedBoards} + newCollapsedBoards[board.id] = !newCollapsedBoards[board.id] + this.setState({collapsedBoards: newCollapsedBoards}) + }} /> -
} - {!collapsedBoards[board.id] && boardViews.map((view) => ( -
-
{ - this.viewClicked(board, view) + this.boardClicked(board) }} - title={view.title || intl.formatMessage({id: 'Sidebar.untitled-view', defaultMessage: '(Untitled View)'})} + title={displayTitle} > - {view.title || intl.formatMessage({id: 'Sidebar.untitled-view', defaultMessage: '(Untitled View)'})} + {board.icon ? `${board.icon} ${displayTitle}` : displayTitle}
+ + }/> + + } + onClick={async () => { + const nextBoardId = boards.length > 1 ? boards.find((o) => o.id !== board.id)?.id : undefined + mutator.deleteBlock( + board, + 'delete block', + async () => { + nextBoardId && this.props.showBoard(nextBoardId!) + }, + async () => { + this.props.showBoard(board.id) + }, + ) + }} + /> + + } + onClick={async () => { + await mutator.duplicateBoard( + board.id, + 'duplicate board', + async (newBoardId) => { + newBoardId && this.props.showBoard(newBoardId) + }, + async () => { + this.props.showBoard(board.id) + }, + ) + }} + /> + +
- ))} -
- ) - }) - } + {!collapsedBoards[board.id] && boardViews.length === 0 && +
+ +
} + {!collapsedBoards[board.id] && boardViews.map((view) => ( +
+ +
{ + this.viewClicked(board, view) + }} + title={view.title || intl.formatMessage({id: 'Sidebar.untitled-view', defaultMessage: '(Untitled View)'})} + > + {view.title || intl.formatMessage({id: 'Sidebar.untitled-view', defaultMessage: '(Untitled View)'})} +
+
+ ))} +
+ ) + }) + } -
+
- + +
From ab439ab98a53493457991dc0067cf9bcac7c6d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 17 Nov 2020 19:08:37 +0100 Subject: [PATCH 02/23] Fixing emoji picker --- webapp/src/widgets/menu/menu.scss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/webapp/src/widgets/menu/menu.scss b/webapp/src/widgets/menu/menu.scss index 24e4aca46..1ea817583 100644 --- a/webapp/src/widgets/menu/menu.scss +++ b/webapp/src/widgets/menu/menu.scss @@ -33,21 +33,17 @@ flex-direction: row; align-items: center; - white-space: nowrap; font-weight: 400; padding: 2px 10px; cursor: pointer; touch-action: none; - * { - display: flex; - } - &:hover { background: rgba(90, 90, 90, 0.1); } .menu-name { + display: flex; flex-grow: 1; margin-right: 20px; } @@ -60,5 +56,9 @@ height: 16px; margin-right: 5px; } + + .IconButton .Icon { + margin-right: 0; + } } } From f9a7a00eccfefad23291cd84f83ec7df09c4b51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 17 Nov 2020 19:14:56 +0100 Subject: [PATCH 03/23] Fix the new template from card menu entry --- webapp/src/widgets/menu/menu.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/src/widgets/menu/menu.scss b/webapp/src/widgets/menu/menu.scss index 1ea817583..1c9339563 100644 --- a/webapp/src/widgets/menu/menu.scss +++ b/webapp/src/widgets/menu/menu.scss @@ -45,6 +45,7 @@ .menu-name { display: flex; flex-grow: 1; + white-space: nowrap; margin-right: 20px; } From 184b2f1b2574094c347c20e78d73c042b34a83d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 17 Nov 2020 19:40:40 +0100 Subject: [PATCH 04/23] Fixed firefox incompatiblity --- webapp/src/widgets/editable.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/webapp/src/widgets/editable.tsx b/webapp/src/widgets/editable.tsx index 3be215e42..997dda704 100644 --- a/webapp/src/widgets/editable.tsx +++ b/webapp/src/widgets/editable.tsx @@ -24,11 +24,11 @@ export default class Editable extends React.Component { } public focus(): void { - this.elementRef.current!.focus() - - // Put cursor at end - document.execCommand('selectAll', false, undefined) - document.getSelection()?.collapseToEnd() + if (this.elementRef.current) { + const valueLength = this.elementRef.current.value.length + this.elementRef.current.focus() + this.elementRef.current.setSelectionRange(valueLength, valueLength) + } } public blur = (): void => { From c41ec11d0afbf20a92f8999576162c4d01227de7 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Tue, 17 Nov 2020 10:53:46 -0800 Subject: [PATCH 05/23] Handle optional current --- webapp/src/components/boardComponent.tsx | 24 ++++++------- webapp/src/components/editable.tsx | 46 ++++++++++++++---------- webapp/src/components/markdownEditor.tsx | 5 ++- webapp/src/components/tableComponent.tsx | 16 ++++++--- webapp/src/components/tableRow.tsx | 2 +- webapp/src/components/viewHeader.tsx | 6 ++-- webapp/src/widgets/editable.tsx | 2 +- 7 files changed, 60 insertions(+), 41 deletions(-) diff --git a/webapp/src/components/boardComponent.tsx b/webapp/src/components/boardComponent.tsx index 79d19d3f8..6636d4733 100644 --- a/webapp/src/components/boardComponent.tsx +++ b/webapp/src/components/boardComponent.tsx @@ -279,19 +279,19 @@ class BoardComponent extends React.Component { }} onDragOver={(e) => { - ref.current!.classList.add('dragover') + ref.current?.classList.add('dragover') e.preventDefault() }} onDragEnter={(e) => { - ref.current!.classList.add('dragover') + ref.current?.classList.add('dragover') e.preventDefault() }} onDragLeave={(e) => { - ref.current!.classList.remove('dragover') + ref.current?.classList.remove('dragover') e.preventDefault() }} onDrop={(e) => { - ref.current!.classList.remove('dragover') + ref.current?.classList.remove('dragover') e.preventDefault() this.onDropToColumn(group.option) }} @@ -348,19 +348,19 @@ class BoardComponent extends React.Component { }} onDragOver={(e) => { - ref.current!.classList.add('dragover') + ref.current?.classList.add('dragover') e.preventDefault() }} onDragEnter={(e) => { - ref.current!.classList.add('dragover') + ref.current?.classList.add('dragover') e.preventDefault() }} onDragLeave={(e) => { - ref.current!.classList.remove('dragover') + ref.current?.classList.remove('dragover') e.preventDefault() }} onDrop={(e) => { - ref.current!.classList.remove('dragover') + ref.current?.classList.remove('dragover') e.preventDefault() this.onDropToColumn(group.option) }} @@ -424,25 +424,25 @@ class BoardComponent extends React.Component { if (this.draggedCards?.length < 1) { return } - ref.current!.classList.add('dragover') + ref.current?.classList.add('dragover') e.preventDefault() }} onDragEnter={(e) => { if (this.draggedCards?.length < 1) { return } - ref.current!.classList.add('dragover') + ref.current?.classList.add('dragover') e.preventDefault() }} onDragLeave={(e) => { if (this.draggedCards?.length < 1) { return } - ref.current!.classList.remove('dragover') + ref.current?.classList.remove('dragover') e.preventDefault() }} onDrop={(e) => { - ref.current!.classList.remove('dragover') + ref.current?.classList.remove('dragover') e.preventDefault() if (this.draggedCards?.length < 1) { return diff --git a/webapp/src/components/editable.tsx b/webapp/src/components/editable.tsx index e04b0154c..3a99d238d 100644 --- a/webapp/src/components/editable.tsx +++ b/webapp/src/components/editable.tsx @@ -32,12 +32,17 @@ class Editable extends React.PureComponent { return this.privateText } set text(value: string) { + if (!this.elementRef.current) { + Utils.assertFailure('elementRef.current') + return + } + const {isMarkdown} = this.props if (value) { - this.elementRef.current!.innerHTML = isMarkdown ? Utils.htmlFromMarkdown(value) : Utils.htmlEncode(value) + this.elementRef.current.innerHTML = isMarkdown ? Utils.htmlFromMarkdown(value) : Utils.htmlEncode(value) } else { - this.elementRef.current!.innerText = '' + this.elementRef.current.innerText = '' } this.privateText = value || '' @@ -55,7 +60,7 @@ class Editable extends React.PureComponent { } focus(): void { - this.elementRef.current!.focus() + this.elementRef.current?.focus() // Put cursor at end document.execCommand('selectAll', false, undefined) @@ -63,7 +68,7 @@ class Editable extends React.PureComponent { } blur(): void { - this.elementRef.current!.blur() + this.elementRef.current?.blur() } render(): JSX.Element { @@ -90,9 +95,11 @@ class Editable extends React.PureComponent { dangerouslySetInnerHTML={{__html: html}} onFocus={() => { - this.elementRef.current!.innerText = this.text - this.elementRef.current!.style!.color = style?.color || '' - this.elementRef.current!.classList.add('active') + if (this.elementRef.current) { + this.elementRef.current.innerText = this.text + this.elementRef.current.style.color = style?.color || '' + this.elementRef.current.classList.add('active') + } if (onFocus) { onFocus() @@ -100,19 +107,22 @@ class Editable extends React.PureComponent { }} onBlur={async () => { - const newText = this.elementRef.current!.innerText - const oldText = this.props.text || '' - if (this.props.allowEmpty || newText) { - if (newText !== oldText && onChanged) { - onChanged(newText) + if (this.elementRef.current) { + const newText = this.elementRef.current.innerText + const oldText = this.props.text || '' + if (this.props.allowEmpty || newText) { + if (newText !== oldText && onChanged) { + onChanged(newText) + } + + this.text = newText + } else { + this.text = oldText // Reset text } - this.text = newText - } else { - this.text = oldText // Reset text + this.elementRef.current.classList.remove('active') } - this.elementRef.current!.classList.remove('active') if (onBlur) { onBlur() } @@ -121,10 +131,10 @@ class Editable extends React.PureComponent { onKeyDown={(e) => { if (e.keyCode === 27 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // ESC e.stopPropagation() - this.elementRef.current!.blur() + this.elementRef.current?.blur() } else if (!isMultiline && e.keyCode === 13 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // Return e.stopPropagation() - this.elementRef.current!.blur() + this.elementRef.current?.blur() } if (onKeyDown) { diff --git a/webapp/src/components/markdownEditor.tsx b/webapp/src/components/markdownEditor.tsx index 523a24377..f01f1ed06 100644 --- a/webapp/src/components/markdownEditor.tsx +++ b/webapp/src/components/markdownEditor.tsx @@ -152,9 +152,8 @@ class MarkdownEditor extends React.Component { this.hideEditor() }, focus: () => { - this.frameRef.current!.classList.add('active') - - this.elementRef.current!.setState({value: this.text}) + this.frameRef.current?.classList.add('active') + this.elementRef.current?.setState({value: this.text}) if (onFocus) { onFocus() diff --git a/webapp/src/components/tableComponent.tsx b/webapp/src/components/tableComponent.tsx index e183ab07d..325c27d1a 100644 --- a/webapp/src/components/tableComponent.tsx +++ b/webapp/src/components/tableComponent.tsx @@ -127,13 +127,17 @@ class TableComponent extends React.Component { onDrag={(offset) => { const originalWidth = this.columnWidth(Constants.titleColumnId) const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset) - titleRef.current!.style!.width = `${newWidth}px` + if (titleRef.current) { + titleRef.current.style.width = `${newWidth}px` + } }} onDragEnd={(offset) => { Utils.log(`onDragEnd offset: ${offset}`) const originalWidth = this.columnWidth(Constants.titleColumnId) const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset) - titleRef.current!.style!.width = `${newWidth}px` + if (titleRef.current) { + titleRef.current.style.width = `${newWidth}px` + } const columnWidths = {...activeView.columnWidths} if (newWidth !== columnWidths[Constants.titleColumnId]) { @@ -211,13 +215,17 @@ class TableComponent extends React.Component { onDrag={(offset) => { const originalWidth = this.columnWidth(template.id) const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset) - headerRef.current!.style.width = `${newWidth}px` + if (headerRef.current) { + headerRef.current.style.width = `${newWidth}px` + } }} onDragEnd={(offset) => { Utils.log(`onDragEnd offset: ${offset}`) const originalWidth = this.columnWidth(template.id) const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset) - headerRef.current!.style.width = `${newWidth}px` + if (headerRef.current) { + headerRef.current.style.width = `${newWidth}px` + } const columnWidths = {...activeView.columnWidths} if (newWidth !== columnWidths[template.id]) { diff --git a/webapp/src/components/tableRow.tsx b/webapp/src/components/tableRow.tsx index fdbb5aac6..ea0985b6d 100644 --- a/webapp/src/components/tableRow.tsx +++ b/webapp/src/components/tableRow.tsx @@ -40,7 +40,7 @@ class TableRow extends React.Component { componentDidMount(): void { if (this.props.focusOnMount) { - setTimeout(() => this.titleRef.current!.focus(), 10) + setTimeout(() => this.titleRef.current?.focus(), 10) } } diff --git a/webapp/src/components/viewHeader.tsx b/webapp/src/components/viewHeader.tsx index 5c65c6bb7..44603c7ed 100644 --- a/webapp/src/components/viewHeader.tsx +++ b/webapp/src/components/viewHeader.tsx @@ -61,7 +61,7 @@ class ViewHeader extends React.Component { componentDidUpdate(prevPros: Props, prevState: State): void { if (this.state.isSearching && !prevState.isSearching) { - this.searchFieldRef.current!.focus() + this.searchFieldRef.current?.focus() } } @@ -75,7 +75,9 @@ class ViewHeader extends React.Component { private onSearchKeyDown = (e: React.KeyboardEvent) => { if (e.keyCode === 27) { // ESC: Clear search - this.searchFieldRef.current!.text = '' + if (this.searchFieldRef.current) { + this.searchFieldRef.current.text = '' + } this.setState({isSearching: false}) this.props.setSearchText(undefined) e.preventDefault() diff --git a/webapp/src/widgets/editable.tsx b/webapp/src/widgets/editable.tsx index 997dda704..43f30a20a 100644 --- a/webapp/src/widgets/editable.tsx +++ b/webapp/src/widgets/editable.tsx @@ -33,7 +33,7 @@ export default class Editable extends React.Component { public blur = (): void => { this.saveOnBlur = false - this.elementRef.current!.blur() + this.elementRef.current?.blur() this.saveOnBlur = true } From e9e8818738658e04fe2ff12533d0f00c1413468f Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Tue, 17 Nov 2020 11:17:44 -0800 Subject: [PATCH 06/23] Use template icon for new cards if set --- webapp/src/components/boardComponent.tsx | 4 +++- webapp/src/components/tableComponent.tsx | 4 +++- webapp/src/components/viewHeader.tsx | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/boardComponent.tsx b/webapp/src/components/boardComponent.tsx index 6636d4733..ac55cea07 100644 --- a/webapp/src/components/boardComponent.tsx +++ b/webapp/src/components/boardComponent.tsx @@ -511,7 +511,9 @@ class BoardComponent extends React.Component { } } card.properties = {...card.properties, ...propertiesThatMeetFilters} - card.icon = BlockIcons.shared.randomIcon() + if (!card.icon) { + card.icon = BlockIcons.shared.randomIcon() + } await mutator.insertBlocks( blocksToInsert, 'add card', diff --git a/webapp/src/components/tableComponent.tsx b/webapp/src/components/tableComponent.tsx index 325c27d1a..01cd2b9cc 100644 --- a/webapp/src/components/tableComponent.tsx +++ b/webapp/src/components/tableComponent.tsx @@ -327,7 +327,9 @@ class TableComponent extends React.Component { } card.parentId = boardTree.board.id - card.icon = BlockIcons.shared.randomIcon() + if (!card.icon) { + card.icon = BlockIcons.shared.randomIcon() + } await mutator.insertBlocks( blocksToInsert, 'add card', diff --git a/webapp/src/components/viewHeader.tsx b/webapp/src/components/viewHeader.tsx index 44603c7ed..cb8868b52 100644 --- a/webapp/src/components/viewHeader.tsx +++ b/webapp/src/components/viewHeader.tsx @@ -399,11 +399,15 @@ class ViewHeader extends React.Component { {boardTree.cardTemplates.map((cardTemplate) => { + let displayName = cardTemplate.title || intl.formatMessage({id: 'ViewHeader.untitled', defaultMessage: 'Untitled'}) + if (cardTemplate.icon) { + displayName = `${cardTemplate.icon} ${displayName}` + } return ( { this.props.addCardFromTemplate(cardTemplate.id) }} From a704dde73395963b157e38769847a61c3ba51892 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Tue, 17 Nov 2020 11:48:59 -0800 Subject: [PATCH 07/23] Fix bug: Update TableRow when card updated in dialog --- webapp/src/components/tableComponent.tsx | 2 +- webapp/src/components/tableRow.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/webapp/src/components/tableComponent.tsx b/webapp/src/components/tableComponent.tsx index 01cd2b9cc..740e696e5 100644 --- a/webapp/src/components/tableComponent.tsx +++ b/webapp/src/components/tableComponent.tsx @@ -254,7 +254,7 @@ class TableComponent extends React.Component { const tableRow = ( { return (
{/* Name / title */} From 02d26a800ab782075d9ac7944e7743601fdf0dca Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Tue, 17 Nov 2020 14:11:04 -0800 Subject: [PATCH 08/23] Board templates --- webapp/src/blocks/board.ts | 17 +++ webapp/src/blocks/card.ts | 2 +- webapp/src/components/sidebar.scss | 8 +- webapp/src/components/sidebar.tsx | 131 +++++++++++++++--- webapp/src/components/viewHeader.tsx | 23 +++ webapp/src/components/workspaceComponent.scss | 14 +- webapp/src/components/workspaceComponent.tsx | 13 +- webapp/src/mutator.ts | 16 +-- webapp/src/octoUtils.tsx | 7 +- webapp/src/pages/boardPage.tsx | 2 +- webapp/src/viewModel/boardTree.ts | 9 ++ webapp/src/viewModel/workspaceTree.ts | 7 +- webapp/src/widgets/buttons/iconButton.scss | 1 - webapp/src/widgets/menu/menu.scss | 1 + 14 files changed, 214 insertions(+), 37 deletions(-) diff --git a/webapp/src/blocks/board.ts b/webapp/src/blocks/board.ts index 168dc9390..5f0da1476 100644 --- a/webapp/src/blocks/board.ts +++ b/webapp/src/blocks/board.ts @@ -1,5 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {Utils} from '../utils' + import {IBlock} from '../blocks/block' import {MutableBlock} from './block' @@ -35,7 +37,9 @@ interface IMutablePropertyTemplate extends IPropertyTemplate { interface Board extends IBlock { readonly icon: string + readonly isTemplate: boolean readonly cardProperties: readonly IPropertyTemplate[] + duplicate(): MutableBoard } class MutableBoard extends MutableBlock { @@ -46,6 +50,13 @@ class MutableBoard extends MutableBlock { this.fields.icon = value } + get isTemplate(): boolean { + return Boolean(this.fields.isTemplate) + } + set isTemplate(value: boolean) { + this.fields.isTemplate = value + } + get cardProperties(): IMutablePropertyTemplate[] { return this.fields.cardProperties as IPropertyTemplate[] } @@ -72,6 +83,12 @@ class MutableBoard extends MutableBlock { this.cardProperties = [] } } + + duplicate(): MutableBoard { + const card = new MutableBoard(this) + card.id = Utils.createGuid() + return card + } } export {Board, MutableBoard, PropertyType, IPropertyOption, IPropertyTemplate} diff --git a/webapp/src/blocks/card.ts b/webapp/src/blocks/card.ts index 9e0abef4a..f163ceeb5 100644 --- a/webapp/src/blocks/card.ts +++ b/webapp/src/blocks/card.ts @@ -21,7 +21,7 @@ class MutableCard extends MutableBlock { } get isTemplate(): boolean { - return this.fields.isTemplate as boolean + return Boolean(this.fields.isTemplate) } set isTemplate(value: boolean) { this.fields.isTemplate = value diff --git a/webapp/src/components/sidebar.scss b/webapp/src/components/sidebar.scss index 36c2be062..9df4a3ed8 100644 --- a/webapp/src/components/sidebar.scss +++ b/webapp/src/components/sidebar.scss @@ -42,7 +42,7 @@ font-weight: 600; padding: 3px 20px; margin-bottom: 5px; - .IconButton { + >.IconButton { background-color: var(--sidebar-bg); &:hover { background-color: rgba(var(--sidebar-fg), 0.1); @@ -87,7 +87,7 @@ } } - .IconButton { + >.IconButton { background-color: var(--sidebar-bg); &:hover { background-color: rgba(var(--sidebar-fg), 0.1); @@ -127,6 +127,10 @@ flex-shrink: 0; } + .Menu .OptionsIcon { + fill: unset; + } + .HideSidebarIcon { stroke: rgba(var(--sidebar-fg), 0.5); stroke-width: 6px; diff --git a/webapp/src/components/sidebar.tsx b/webapp/src/components/sidebar.tsx index 193edc5dc..40e5acab8 100644 --- a/webapp/src/components/sidebar.tsx +++ b/webapp/src/components/sidebar.tsx @@ -4,21 +4,23 @@ import React from 'react' import {FormattedMessage, injectIntl, IntlShape} from 'react-intl' import {Archiver} from '../archiver' +import {IBlock} from '../blocks/block' import {Board, MutableBoard} from '../blocks/board' import {BoardView, MutableBoardView} from '../blocks/boardView' import mutator from '../mutator' import {darkTheme, lightTheme, mattermostTheme, setTheme} from '../theme' +import {MutableBoardTree} from '../viewModel/boardTree' import {WorkspaceTree} from '../viewModel/workspaceTree' import Button from '../widgets/buttons/button' import IconButton from '../widgets/buttons/iconButton' import DeleteIcon from '../widgets/icons/delete' +import DisclosureTriangle from '../widgets/icons/disclosureTriangle' import DotIcon from '../widgets/icons/dot' import DuplicateIcon from '../widgets/icons/duplicate' import HamburgerIcon from '../widgets/icons/hamburger' import HideSidebarIcon from '../widgets/icons/hideSidebar' import OptionsIcon from '../widgets/icons/options' import ShowSidebarIcon from '../widgets/icons/showSidebar' -import DisclosureTriangle from '../widgets/icons/disclosureTriangle' import Menu from '../widgets/menu' import MenuWrapper from '../widgets/menuWrapper' import './sidebar.scss' @@ -185,14 +187,77 @@ class Sidebar extends React.Component {
- + + + + + + + + + + + + {workspaceTree.boardTemplates.map((boardTemplate) => { + let displayName = boardTemplate.title || intl.formatMessage({id: 'Sidebar.untitled', defaultMessage: 'Untitled'}) + if (boardTemplate.icon) { + displayName = `${boardTemplate.icon} ${displayName}` + } + return ( + { + this.addBoardClicked(boardTemplate.id) + }} + rightIcon={ + + }/> + + { + this.props.showBoard(boardTemplate.id) + }} + /> + } + id='delete' + name={intl.formatMessage({id: 'Sidebar.delete-template', defaultMessage: 'Delete'})} + onClick={async () => { + await mutator.deleteBlock(boardTemplate, 'delete board template') + }} + /> + + + } + /> + ) + })} + + + + + +
@@ -266,18 +331,34 @@ class Sidebar extends React.Component { this.props.showView(view.id, board.id) } - private addBoardClicked = async () => { + private addBoardClicked = async (boardTemplateId?: string) => { const {showBoard, intl} = this.props const oldBoardId = this.props.activeBoardId - const board = new MutableBoard() - const view = new MutableBoardView() - view.viewType = 'board' - view.parentId = board.id - view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board View'}) + let board: MutableBoard + const blocksToInsert: IBlock[] = [] + + if (boardTemplateId) { + const templateBoardTree = new MutableBoardTree(boardTemplateId) + await templateBoardTree.sync() + const newBoardTree = templateBoardTree.templateCopy() + board = newBoardTree.board + board.isTemplate = false + board.title = '' + blocksToInsert.push(...newBoardTree.allBlocks) + } else { + board = new MutableBoard() + blocksToInsert.push(board) + + const view = new MutableBoardView() + view.viewType = 'board' + view.parentId = board.id + view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board View'}) + blocksToInsert.push(view) + } await mutator.insertBlocks( - [board, view], + blocksToInsert, 'add board', async () => { showBoard(board.id) @@ -290,6 +371,24 @@ class Sidebar extends React.Component { ) } + private addBoardTemplateClicked = async () => { + const {activeBoardId} = this.props + + const boardTemplate = new MutableBoard() + boardTemplate.isTemplate = true + await mutator.insertBlock( + boardTemplate, + 'add board template', + async () => { + this.props.showBoard(boardTemplate.id) + }, async () => { + if (activeBoardId) { + this.props.showBoard(activeBoardId) + } + }, + ) + } + private hideClicked = () => { this.setState({isHidden: true}) } diff --git a/webapp/src/components/viewHeader.tsx b/webapp/src/components/viewHeader.tsx index cb8868b52..2e749387d 100644 --- a/webapp/src/components/viewHeader.tsx +++ b/webapp/src/components/viewHeader.tsx @@ -3,6 +3,8 @@ import React from 'react' import {FormattedMessage, injectIntl, IntlShape} from 'react-intl' +import {Utils} from '../utils' + import {Archiver} from '../archiver' import {BlockIcons} from '../blockIcons' import {IPropertyTemplate} from '../blocks/board' @@ -349,6 +351,11 @@ class ViewHeader extends React.Component { name={intl.formatMessage({id: 'ViewHeader.export-board-archive', defaultMessage: 'Export Board Archive'})} onClick={() => Archiver.exportBoardTree(boardTree)} /> + @@ -464,6 +471,22 @@ class ViewHeader extends React.Component { return options } + + private newTemplateFromBoardClicked = async () => { + const {boardTree} = this.props + + const newBoardTree = boardTree.templateCopy() + newBoardTree.board.isTemplate = true + newBoardTree.board.title = 'New Board Template' + + Utils.log(`Created new board template: ${newBoardTree.board.id}`) + + const blocksToInsert = newBoardTree.allBlocks + await mutator.insertBlocks( + blocksToInsert, + 'create template from board', + ) + } } export default injectIntl(ViewHeader) diff --git a/webapp/src/components/workspaceComponent.scss b/webapp/src/components/workspaceComponent.scss index 8a1ed13c8..5fe358c2e 100644 --- a/webapp/src/components/workspaceComponent.scss +++ b/webapp/src/components/workspaceComponent.scss @@ -2,5 +2,17 @@ flex: 1 1 auto; display: flex; flex-direction: row; - overflow: auto; + overflow: auto; + + > .mainFrame { + flex: 1 1 auto; + display: flex; + flex-direction: column; + + > .banner { + background-color: rgba(230, 220, 192, 0.9); + text-align: center; + padding: 10px; + } + } } diff --git a/webapp/src/components/workspaceComponent.tsx b/webapp/src/components/workspaceComponent.tsx index e1bf0f0fa..6c5450de5 100644 --- a/webapp/src/components/workspaceComponent.tsx +++ b/webapp/src/components/workspaceComponent.tsx @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React from 'react' +import {FormattedMessage} from 'react-intl' import {Utils} from '../utils' import {BoardTree} from '../viewModel/boardTree' @@ -34,7 +35,17 @@ class WorkspaceComponent extends React.PureComponent { activeBoardId={boardTree?.board.id} setLanguage={setLanguage} /> - {this.mainComponent()} +
+ {(boardTree?.board.isTemplate) && +
+ +
+ } + {this.mainComponent()} +
) return element diff --git a/webapp/src/mutator.ts b/webapp/src/mutator.ts index 8950d90b2..8dd46c92f 100644 --- a/webapp/src/mutator.ts +++ b/webapp/src/mutator.ts @@ -486,40 +486,36 @@ class Mutator { async duplicateCard(cardId: string, description = 'duplicate card', afterRedo?: (newBoardId: string) => Promise, beforeUndo?: () => Promise): Promise<[IBlock[], string]> { const blocks = await octoClient.getSubtree(cardId, 2) - const [newBlocks1, idMap] = OctoUtils.duplicateBlockTree(blocks, cardId) + const [newBlocks1, newCard] = OctoUtils.duplicateBlockTree(blocks, cardId) const newBlocks = newBlocks1.filter((o) => o.type !== 'comment') Utils.log(`duplicateCard: duplicating ${newBlocks.length} blocks`) - const newCardId = idMap[cardId] - const newCard = newBlocks.find((o) => o.id === newCardId)! newCard.title = `Copy of ${newCard.title}` await this.insertBlocks( newBlocks, description, async () => { - await afterRedo?.(newCardId) + await afterRedo?.(newCard.id) }, beforeUndo, ) - return [newBlocks, newCardId] + return [newBlocks, newCard.id] } async duplicateBoard(boardId: string, description = 'duplicate board', afterRedo?: (newBoardId: string) => Promise, beforeUndo?: () => Promise): Promise<[IBlock[], string]> { const blocks = await octoClient.getSubtree(boardId, 3) - const [newBlocks1, idMap] = OctoUtils.duplicateBlockTree(blocks, boardId) + const [newBlocks1, newBoard] = OctoUtils.duplicateBlockTree(blocks, boardId) const newBlocks = newBlocks1.filter((o) => o.type !== 'comment') Utils.log(`duplicateBoard: duplicating ${newBlocks.length} blocks`) - const newBoardId = idMap[boardId] - const newBoard = newBlocks.find((o) => o.id === newBoardId)! newBoard.title = `Copy of ${newBoard.title}` await this.insertBlocks( newBlocks, description, async () => { - await afterRedo?.(newBoardId) + await afterRedo?.(newBoard.id) }, beforeUndo, ) - return [newBlocks, newBoardId] + return [newBlocks, newBoard.id] } // Other methods diff --git a/webapp/src/octoUtils.tsx b/webapp/src/octoUtils.tsx index 70f3296e1..27b154590 100644 --- a/webapp/src/octoUtils.tsx +++ b/webapp/src/octoUtils.tsx @@ -88,7 +88,7 @@ class OctoUtils { } // Creates a copy of the blocks with new ids and parentIDs - static duplicateBlockTree(blocks: IBlock[], rootBlockId?: string): [MutableBlock[], Readonly>] { + static duplicateBlockTree(blocks: IBlock[], rootBlockId: string): [MutableBlock[], MutableBlock, Readonly>] { const idMap: Record = {} const newBlocks = blocks.map((block) => { const newBlock = this.hydrateBlock(block) @@ -97,7 +97,7 @@ class OctoUtils { return newBlock }) - const newRootBlockId = rootBlockId ? idMap[rootBlockId] : undefined + const newRootBlockId = idMap[rootBlockId] newBlocks.forEach((newBlock) => { // Note: Don't remap the parent of the new root block if (newBlock.id !== newRootBlockId && newBlock.parentId) { @@ -112,7 +112,8 @@ class OctoUtils { } }) - return [newBlocks, idMap] + const newRootBlock = newBlocks.find((block) => block.id === newRootBlockId)! + return [newBlocks, newRootBlock, idMap] } } diff --git a/webapp/src/pages/boardPage.tsx b/webapp/src/pages/boardPage.tsx index 166b5000f..87e42b83b 100644 --- a/webapp/src/pages/boardPage.tsx +++ b/webapp/src/pages/boardPage.tsx @@ -143,7 +143,7 @@ export default class BoardPage extends React.Component { const workspaceTree = new MutableWorkspaceTree() await workspaceTree.sync() - const boardIds = workspaceTree.boards.map((o) => o.id) + const boardIds = [...workspaceTree.boards.map((o) => o.id), ...workspaceTree.boardTemplates.map((o) => o.id)] this.setState({workspaceTree}) // Listen to boards plus all blocks at root (Empty string for parentId) diff --git a/webapp/src/viewModel/boardTree.ts b/webapp/src/viewModel/boardTree.ts index f291e3f59..9911b0010 100644 --- a/webapp/src/viewModel/boardTree.ts +++ b/webapp/src/viewModel/boardTree.ts @@ -32,6 +32,7 @@ interface BoardTree { orderedCards(): Card[] mutableCopy(): MutableBoardTree + templateCopy(): MutableBoardTree } class MutableBoardTree implements BoardTree { @@ -405,6 +406,14 @@ class MutableBoardTree implements BoardTree { boardTree.incrementalUpdate(this.rawBlocks) return boardTree } + + templateCopy(): MutableBoardTree { + const [newBlocks, newBoard] = OctoUtils.duplicateBlockTree(this.allBlocks, this.board.id) + + const boardTree = new MutableBoardTree(newBoard.id) + boardTree.incrementalUpdate(newBlocks) + return boardTree + } } export {MutableBoardTree, BoardTree, Group as BoardTreeGroup} diff --git a/webapp/src/viewModel/workspaceTree.ts b/webapp/src/viewModel/workspaceTree.ts index 0bd67001c..f62aeca81 100644 --- a/webapp/src/viewModel/workspaceTree.ts +++ b/webapp/src/viewModel/workspaceTree.ts @@ -8,6 +8,7 @@ import {OctoUtils} from '../octoUtils' interface WorkspaceTree { readonly boards: readonly Board[] + readonly boardTemplates: readonly Board[] readonly views: readonly BoardView[] mutableCopy(): MutableWorkspaceTree @@ -15,6 +16,7 @@ interface WorkspaceTree { class MutableWorkspaceTree { boards: Board[] = [] + boardTemplates: Board[] = [] views: BoardView[] = [] private rawBlocks: IBlock[] = [] @@ -37,7 +39,10 @@ class MutableWorkspaceTree { } private rebuild(blocks: IBlock[]) { - this.boards = blocks.filter((block) => block.type === 'board'). + const allBoards = blocks.filter((block) => block.type === 'board') as Board[] + this.boards = allBoards.filter((block) => !block.isTemplate). + sort((a, b) => a.title.localeCompare(b.title)) as Board[] + this.boardTemplates = allBoards.filter((block) => block.isTemplate). sort((a, b) => a.title.localeCompare(b.title)) as Board[] this.views = blocks.filter((block) => block.type === 'view'). sort((a, b) => a.title.localeCompare(b.title)) as BoardView[] diff --git a/webapp/src/widgets/buttons/iconButton.scss b/webapp/src/widgets/buttons/iconButton.scss index 08f44c569..c3a769e35 100644 --- a/webapp/src/widgets/buttons/iconButton.scss +++ b/webapp/src/widgets/buttons/iconButton.scss @@ -1,7 +1,6 @@ .IconButton { height: 24px; width: 24px; - background-color: rgba(var(--main-fg), 0.1); padding: 0; margin: 0; .Icon { diff --git a/webapp/src/widgets/menu/menu.scss b/webapp/src/widgets/menu/menu.scss index 1c9339563..96772e2b2 100644 --- a/webapp/src/widgets/menu/menu.scss +++ b/webapp/src/widgets/menu/menu.scss @@ -52,6 +52,7 @@ .SubmenuTriangleIcon { fill: rgba(var(--main-fg), 0.7); } + .Icon { width: 16px; height: 16px; From 3961a8a314e188e19e89cfd5af4a040d45d46152 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Tue, 17 Nov 2020 14:16:53 -0800 Subject: [PATCH 09/23] Refactor templateCopy --- webapp/src/viewModel/boardTree.ts | 2 +- webapp/src/viewModel/cardTree.ts | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/webapp/src/viewModel/boardTree.ts b/webapp/src/viewModel/boardTree.ts index 9911b0010..a285401d8 100644 --- a/webapp/src/viewModel/boardTree.ts +++ b/webapp/src/viewModel/boardTree.ts @@ -408,7 +408,7 @@ class MutableBoardTree implements BoardTree { } templateCopy(): MutableBoardTree { - const [newBlocks, newBoard] = OctoUtils.duplicateBlockTree(this.allBlocks, this.board.id) + const [newBlocks, newBoard] = OctoUtils.duplicateBlockTree(this.rawBlocks, this.board.id) const boardTree = new MutableBoardTree(newBoard.id) boardTree.incrementalUpdate(newBlocks) diff --git a/webapp/src/viewModel/cardTree.ts b/webapp/src/viewModel/cardTree.ts index 1ec470da6..89385b48c 100644 --- a/webapp/src/viewModel/cardTree.ts +++ b/webapp/src/viewModel/cardTree.ts @@ -58,16 +58,10 @@ class MutableCardTree implements CardTree { } templateCopy(): MutableCardTree { - const card = this.card.duplicate() + const [newBlocks, newCard] = OctoUtils.duplicateBlockTree(this.rawBlocks, this.card.id) - const contents: IOrderedBlock[] = this.contents.map((content) => { - const copy = MutableBlock.duplicate(content) - copy.parentId = card.id - return copy as IOrderedBlock - }) - - const cardTree = new MutableCardTree(card.id) - cardTree.incrementalUpdate([card, ...contents]) + const cardTree = new MutableCardTree(newCard.id) + cardTree.incrementalUpdate(newBlocks) return cardTree } } From f4a350b207a3443a527e444e89e735eb0e926be8 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Tue, 17 Nov 2020 14:20:37 -0800 Subject: [PATCH 10/23] cleanup imports --- webapp/src/components/viewHeader.tsx | 3 +-- webapp/src/viewModel/cardTree.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/viewHeader.tsx b/webapp/src/components/viewHeader.tsx index 2e749387d..1ed016853 100644 --- a/webapp/src/components/viewHeader.tsx +++ b/webapp/src/components/viewHeader.tsx @@ -3,8 +3,6 @@ import React from 'react' import {FormattedMessage, injectIntl, IntlShape} from 'react-intl' -import {Utils} from '../utils' - import {Archiver} from '../archiver' import {BlockIcons} from '../blockIcons' import {IPropertyTemplate} from '../blocks/board' @@ -15,6 +13,7 @@ import ViewMenu from '../components/viewMenu' import {Constants} from '../constants' import {CsvExporter} from '../csvExporter' import mutator from '../mutator' +import {Utils} from '../utils' import {BoardTree} from '../viewModel/boardTree' import Button from '../widgets/buttons/button' import ButtonWithMenu from '../widgets/buttons/buttonWithMenu' diff --git a/webapp/src/viewModel/cardTree.ts b/webapp/src/viewModel/cardTree.ts index 89385b48c..6659de128 100644 --- a/webapp/src/viewModel/cardTree.ts +++ b/webapp/src/viewModel/cardTree.ts @@ -1,6 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {IBlock, MutableBlock} from '../blocks/block' +import {IBlock} from '../blocks/block' import {Card, MutableCard} from '../blocks/card' import {IOrderedBlock} from '../blocks/orderedBlock' import octoClient from '../octoClient' From fb5421bea3e383f9234596c1a758733ef0390fb9 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Tue, 17 Nov 2020 14:24:23 -0800 Subject: [PATCH 11/23] Show message when creating template from board --- webapp/src/components/viewHeader.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webapp/src/components/viewHeader.tsx b/webapp/src/components/viewHeader.tsx index 1ed016853..a41f91af5 100644 --- a/webapp/src/components/viewHeader.tsx +++ b/webapp/src/components/viewHeader.tsx @@ -29,6 +29,7 @@ import MenuWrapper from '../widgets/menuWrapper' import {Editable} from './editable' import FilterComponent from './filterComponent' +import {sendFlashMessage} from './flashMessages' import './viewHeader.scss' type Props = { @@ -485,6 +486,9 @@ class ViewHeader extends React.Component { blocksToInsert, 'create template from board', ) + + // TODO: Navigate to board editor + sendFlashMessage({content: 'New board template added', severity: 'low'}) } } From d8d6dfef649f5222e9610adf153832f1e4c8c9da Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Wed, 18 Nov 2020 09:52:31 -0800 Subject: [PATCH 12/23] Refactor templates to use mutator.duplicateBoard/Card --- webapp/i18n/en.json | 15 +++- webapp/src/components/boardComponent.tsx | 39 ++++----- webapp/src/components/cardDialog.tsx | 27 +++--- webapp/src/components/sidebar.tsx | 107 +++++++++++++++-------- webapp/src/components/tableComponent.tsx | 44 +++++----- webapp/src/components/viewHeader.tsx | 28 +----- webapp/src/mutator.ts | 43 +++++++-- webapp/src/viewModel/boardTree.ts | 9 -- webapp/src/viewModel/cardTree.ts | 9 -- 9 files changed, 172 insertions(+), 149 deletions(-) diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 028323185..a314849ea 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -25,20 +25,32 @@ "Filter.not-includes": "doesn't include", "FilterComponent.add-filter": "+ Add Filter", "FilterComponent.delete": "Delete", + "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", "Sidebar.add-board": "+ Add Board", + "Sidebar.add-template": "+ New template", "Sidebar.dark-theme": "Dark Theme", "Sidebar.delete-board": "Delete Board", + "Sidebar.delete-template": "Delete", "Sidebar.duplicate-board": "Duplicate Board", + "Sidebar.edit-template": "Edit", + "Sidebar.empty-board": "Empty board", "Sidebar.english": "English", "Sidebar.export-archive": "Export Archive", "Sidebar.import-archive": "Import Archive", "Sidebar.light-theme": "Light Theme", "Sidebar.mattermost-theme": "Mattermost Theme", "Sidebar.no-views-in-board": "No pages inside", + "Sidebar.select-a-template": "Select a template", "Sidebar.set-language": "Set Language", "Sidebar.set-theme": "Set Theme", "Sidebar.settings": "Settings", "Sidebar.spanish": "Spanish", + "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", @@ -76,5 +88,6 @@ "ViewTitle.pick-icon": "Pick Icon", "ViewTitle.random-icon": "Random", "ViewTitle.remove-icon": "Remove Icon", - "ViewTitle.untitled-board": "Untitled Board" + "ViewTitle.untitled-board": "Untitled Board", + "WorkspaceComponent.editing-board-template": "You're editing a board template" } \ No newline at end of file diff --git a/webapp/src/components/boardComponent.tsx b/webapp/src/components/boardComponent.tsx index ac55cea07..fb954f52d 100644 --- a/webapp/src/components/boardComponent.tsx +++ b/webapp/src/components/boardComponent.tsx @@ -5,7 +5,6 @@ import React from 'react' import {FormattedMessage, injectIntl, IntlShape} from 'react-intl' import {BlockIcons} from '../blockIcons' -import {IBlock} from '../blocks/block' import {IPropertyOption, IPropertyTemplate} from '../blocks/board' import {Card, MutableCard} from '../blocks/card' import {CardFilter} from '../cardFilter' @@ -13,7 +12,6 @@ import {Constants} from '../constants' import mutator from '../mutator' import {Utils} from '../utils' import {BoardTree, BoardTreeGroup} from '../viewModel/boardTree' -import {MutableCardTree} from '../viewModel/cardTree' import Button from '../widgets/buttons/button' import IconButton from '../widgets/buttons/iconButton' import AddIcon from '../widgets/icons/add' @@ -27,7 +25,7 @@ import MenuWrapper from '../widgets/menuWrapper' import BoardCard from './boardCard' import {BoardColumn} from './boardColumn' import './boardComponent.scss' -import {CardDialog} from './cardDialog' +import CardDialog from './cardDialog' import {Editable} from './editable' import RootPortal from './rootPortal' import ViewHeader from './viewHeader' @@ -478,28 +476,25 @@ class BoardComponent extends React.Component { } } - private addCardFromTemplate = async (cardTemplateId?: string) => { - this.addCard(undefined, cardTemplateId) + private addCardFromTemplate = async (cardTemplateId: string) => { + await mutator.duplicateCard( + cardTemplateId, + this.props.intl.formatMessage({id: 'Mutator.new-card-from-template', defaultMessage: 'new card from template'}), + false, + async (newCardId) => { + this.setState({shownCardId: newCardId}) + }, + async () => { + this.setState({shownCardId: undefined}) + }, + ) } - private async addCard(groupByOptionId?: string, cardTemplateId?: string): Promise { + private async addCard(groupByOptionId?: string): Promise { const {boardTree} = this.props const {activeView, board} = boardTree - let card: MutableCard - let blocksToInsert: IBlock[] - if (cardTemplateId) { - const templateCardTree = new MutableCardTree(cardTemplateId) - await templateCardTree.sync() - const newCardTree = templateCardTree.templateCopy() - card = newCardTree.card - card.isTemplate = false - card.title = '' - blocksToInsert = [newCardTree.card, ...newCardTree.contents] - } else { - card = new MutableCard() - blocksToInsert = [card] - } + const card = new MutableCard() card.parentId = boardTree.board.id const propertiesThatMeetFilters = CardFilter.propertiesThatMeetFilterGroup(activeView.filter, board.cardProperties) @@ -514,8 +509,8 @@ class BoardComponent extends React.Component { if (!card.icon) { card.icon = BlockIcons.shared.randomIcon() } - await mutator.insertBlocks( - blocksToInsert, + await mutator.insertBlock( + card, 'add card', async () => { this.setState({shownCardId: card.id}) diff --git a/webapp/src/components/cardDialog.tsx b/webapp/src/components/cardDialog.tsx index 5e19542d4..b6edf2675 100644 --- a/webapp/src/components/cardDialog.tsx +++ b/webapp/src/components/cardDialog.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React from 'react' -import {FormattedMessage} from 'react-intl' +import {FormattedMessage, injectIntl, IntlShape} from 'react-intl' import mutator from '../mutator' import {OctoListener} from '../octoListener' @@ -19,6 +19,7 @@ type Props = { cardId: string onClose: () => void showCard: (cardId?: string) => void + intl: IntlShape } type State = { @@ -94,7 +95,7 @@ class CardDialog extends React.Component { } @@ -122,25 +123,19 @@ class CardDialog extends React.Component { ) } - private makeTemplate = async () => { + private makeTemplateClicked = async () => { const {cardTree} = this.state if (!cardTree) { Utils.assertFailure('this.state.cardTree') return } - const newCardTree = cardTree.templateCopy() - newCardTree.card.isTemplate = true - newCardTree.card.title = 'New Template' - - Utils.log(`Created new template: ${newCardTree.card.id}`) - - const blocksToInsert = [newCardTree.card, ...newCardTree.contents] - await mutator.insertBlocks( - blocksToInsert, - 'create template from card', - async () => { - this.props.showCard(newCardTree.card.id) + await mutator.duplicateCard( + cardTree.card.id, + this.props.intl.formatMessage({id: 'Mutator.new-template-from-card', defaultMessage: 'new template from card'}), + true, + async (newCardId) => { + this.props.showCard(newCardId) }, async () => { this.props.showCard(undefined) @@ -149,4 +144,4 @@ class CardDialog extends React.Component { } } -export {CardDialog} +export default injectIntl(CardDialog) diff --git a/webapp/src/components/sidebar.tsx b/webapp/src/components/sidebar.tsx index 40e5acab8..ee26d1ded 100644 --- a/webapp/src/components/sidebar.tsx +++ b/webapp/src/components/sidebar.tsx @@ -4,12 +4,10 @@ import React from 'react' import {FormattedMessage, injectIntl, IntlShape} from 'react-intl' import {Archiver} from '../archiver' -import {IBlock} from '../blocks/block' import {Board, MutableBoard} from '../blocks/board' import {BoardView, MutableBoardView} from '../blocks/boardView' import mutator from '../mutator' import {darkTheme, lightTheme, mattermostTheme, setTheme} from '../theme' -import {MutableBoardTree} from '../viewModel/boardTree' import {WorkspaceTree} from '../viewModel/workspaceTree' import Button from '../widgets/buttons/button' import IconButton from '../widgets/buttons/iconButton' @@ -140,17 +138,16 @@ class Sidebar extends React.Component { id='duplicateBoard' name={intl.formatMessage({id: 'Sidebar.duplicate-board', defaultMessage: 'Duplicate Board'})} icon={} - onClick={async () => { - await mutator.duplicateBoard( - board.id, - 'duplicate board', - async (newBoardId) => { - newBoardId && this.props.showBoard(newBoardId) - }, - async () => { - this.props.showBoard(board.id) - }, - ) + onClick={() => { + this.duplicateBoard(board.id) + }} + /> + + { + this.addTemplateFromBoard(board.id) }} /> @@ -217,7 +214,7 @@ class Sidebar extends React.Component { id={boardTemplate.id} name={displayName} onClick={() => { - this.addBoardClicked(boardTemplate.id) + this.addBoardFromTemplate(boardTemplate.id) }} rightIcon={ @@ -331,34 +328,20 @@ class Sidebar extends React.Component { this.props.showView(view.id, board.id) } - private addBoardClicked = async (boardTemplateId?: string) => { + private addBoardClicked = async () => { const {showBoard, intl} = this.props const oldBoardId = this.props.activeBoardId - let board: MutableBoard - const blocksToInsert: IBlock[] = [] - if (boardTemplateId) { - const templateBoardTree = new MutableBoardTree(boardTemplateId) - await templateBoardTree.sync() - const newBoardTree = templateBoardTree.templateCopy() - board = newBoardTree.board - board.isTemplate = false - board.title = '' - blocksToInsert.push(...newBoardTree.allBlocks) - } else { - board = new MutableBoard() - blocksToInsert.push(board) + const board = new MutableBoard() - const view = new MutableBoardView() - view.viewType = 'board' - view.parentId = board.id - view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board View'}) - blocksToInsert.push(view) - } + const view = new MutableBoardView() + view.viewType = 'board' + view.parentId = board.id + view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board View'}) await mutator.insertBlocks( - blocksToInsert, + [board, view], 'add board', async () => { showBoard(board.id) @@ -371,6 +354,60 @@ class Sidebar extends React.Component { ) } + private async addBoardFromTemplate(boardTemplateId: string) { + const oldBoardId = this.props.activeBoardId + + await mutator.duplicateBoard( + boardTemplateId, + this.props.intl.formatMessage({id: 'Mutator.new-board-from-template', defaultMessage: 'new board from template'}), + false, + async (newBoardId) => { + this.props.showBoard(newBoardId) + }, + async () => { + if (oldBoardId) { + this.props.showBoard(oldBoardId) + } + }, + ) + } + + private async duplicateBoard(boardId: string) { + const oldBoardId = this.props.activeBoardId + + await mutator.duplicateBoard( + boardId, + this.props.intl.formatMessage({id: 'Mutator.duplicate-board', defaultMessage: 'duplicate board'}), + false, + async (newBoardId) => { + this.props.showBoard(newBoardId) + }, + async () => { + if (oldBoardId) { + this.props.showBoard(oldBoardId) + } + }, + ) + } + + private async addTemplateFromBoard(boardId: string) { + const oldBoardId = this.props.activeBoardId + + await mutator.duplicateBoard( + boardId, + this.props.intl.formatMessage({id: 'Mutator.new-template-from-board', defaultMessage: 'new template from board'}), + true, + async (newBoardId) => { + this.props.showBoard(newBoardId) + }, + async () => { + if (oldBoardId) { + this.props.showBoard(oldBoardId) + } + }, + ) + } + private addBoardTemplateClicked = async () => { const {activeBoardId} = this.props diff --git a/webapp/src/components/tableComponent.tsx b/webapp/src/components/tableComponent.tsx index 740e696e5..56cb572bf 100644 --- a/webapp/src/components/tableComponent.tsx +++ b/webapp/src/components/tableComponent.tsx @@ -1,10 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React from 'react' -import {FormattedMessage} from 'react-intl' +import {FormattedMessage, injectIntl, IntlShape} from 'react-intl' import {BlockIcons} from '../blockIcons' -import {IBlock} from '../blocks/block' import {IPropertyTemplate} from '../blocks/board' import {MutableBoardView} from '../blocks/boardView' import {MutableCard} from '../blocks/card' @@ -12,12 +11,11 @@ import {Constants} from '../constants' import mutator from '../mutator' import {Utils} from '../utils' import {BoardTree} from '../viewModel/boardTree' -import {MutableCardTree} from '../viewModel/cardTree' import SortDownIcon from '../widgets/icons/sortDown' import SortUpIcon from '../widgets/icons/sortUp' import MenuWrapper from '../widgets/menuWrapper' -import {CardDialog} from './cardDialog' +import CardDialog from './cardDialog' import {HorizontalGrip} from './horizontalGrip' import RootPortal from './rootPortal' import './tableComponent.scss' @@ -30,6 +28,7 @@ type Props = { boardTree: BoardTree showView: (id: string) => void setSearchText: (text?: string) => void + intl: IntlShape } type State = { @@ -304,34 +303,31 @@ class TableComponent extends React.Component { this.addCard(true) } - private addCardFromTemplate = async (cardTemplateId?: string) => { - this.addCard(true, cardTemplateId) + private addCardFromTemplate = async (cardTemplateId: string) => { + await mutator.duplicateCard( + cardTemplateId, + this.props.intl.formatMessage({id: 'Mutator.new-card-from-template', defaultMessage: 'new card from template'}), + false, + async (newCardId) => { + this.setState({shownCardId: newCardId}) + }, + async () => { + this.setState({shownCardId: undefined}) + }, + ) } - private addCard = async (show = false, cardTemplateId?: string) => { + private addCard = async (show = false) => { const {boardTree} = this.props - let card: MutableCard - let blocksToInsert: IBlock[] - if (cardTemplateId) { - const templateCardTree = new MutableCardTree(cardTemplateId) - await templateCardTree.sync() - const newCardTree = templateCardTree.templateCopy() - card = newCardTree.card - card.isTemplate = false - card.title = '' - blocksToInsert = [newCardTree.card, ...newCardTree.contents] - } else { - card = new MutableCard() - blocksToInsert = [card] - } + const card = new MutableCard() card.parentId = boardTree.board.id if (!card.icon) { card.icon = BlockIcons.shared.randomIcon() } - await mutator.insertBlocks( - blocksToInsert, + await mutator.insertBlock( + card, 'add card', async () => { if (show) { @@ -385,4 +381,4 @@ class TableComponent extends React.Component { } } -export {TableComponent} +export default injectIntl(TableComponent) diff --git a/webapp/src/components/viewHeader.tsx b/webapp/src/components/viewHeader.tsx index a41f91af5..0086fcac5 100644 --- a/webapp/src/components/viewHeader.tsx +++ b/webapp/src/components/viewHeader.tsx @@ -13,7 +13,6 @@ import ViewMenu from '../components/viewMenu' import {Constants} from '../constants' import {CsvExporter} from '../csvExporter' import mutator from '../mutator' -import {Utils} from '../utils' import {BoardTree} from '../viewModel/boardTree' import Button from '../widgets/buttons/button' import ButtonWithMenu from '../widgets/buttons/buttonWithMenu' @@ -29,7 +28,6 @@ import MenuWrapper from '../widgets/menuWrapper' import {Editable} from './editable' import FilterComponent from './filterComponent' -import {sendFlashMessage} from './flashMessages' import './viewHeader.scss' type Props = { @@ -37,7 +35,7 @@ type Props = { showView: (id: string) => void setSearchText: (text?: string) => void addCard: () => void - addCardFromTemplate: (cardTemplateId?: string) => void + addCardFromTemplate: (cardTemplateId: string) => void addCardTemplate: () => void editCardTemplate: (cardTemplateId: string) => void withGroupBy?: boolean @@ -351,11 +349,6 @@ class ViewHeader extends React.Component { name={intl.formatMessage({id: 'ViewHeader.export-board-archive', defaultMessage: 'Export Board Archive'})} onClick={() => Archiver.exportBoardTree(boardTree)} /> - @@ -471,25 +464,6 @@ class ViewHeader extends React.Component { return options } - - private newTemplateFromBoardClicked = async () => { - const {boardTree} = this.props - - const newBoardTree = boardTree.templateCopy() - newBoardTree.board.isTemplate = true - newBoardTree.board.title = 'New Board Template' - - Utils.log(`Created new board template: ${newBoardTree.board.id}`) - - const blocksToInsert = newBoardTree.allBlocks - await mutator.insertBlocks( - blocksToInsert, - 'create template from board', - ) - - // TODO: Navigate to board editor - sendFlashMessage({content: 'New board template added', severity: 'low'}) - } } export default injectIntl(ViewHeader) diff --git a/webapp/src/mutator.ts b/webapp/src/mutator.ts index 8dd46c92f..6aabcbf3b 100644 --- a/webapp/src/mutator.ts +++ b/webapp/src/mutator.ts @@ -484,12 +484,27 @@ class Mutator { // Duplicate - async duplicateCard(cardId: string, description = 'duplicate card', afterRedo?: (newBoardId: string) => Promise, beforeUndo?: () => Promise): Promise<[IBlock[], string]> { + async duplicateCard( + cardId: string, + description = 'duplicate card', + asTemplate = false, + afterRedo?: (newCardId: string) => Promise, + beforeUndo?: () => Promise, + ): Promise<[IBlock[], string]> { const blocks = await octoClient.getSubtree(cardId, 2) - const [newBlocks1, newCard] = OctoUtils.duplicateBlockTree(blocks, cardId) + const [newBlocks1, newCard] = OctoUtils.duplicateBlockTree(blocks, cardId) as [IBlock[], MutableCard, Record] const newBlocks = newBlocks1.filter((o) => o.type !== 'comment') Utils.log(`duplicateCard: duplicating ${newBlocks.length} blocks`) - newCard.title = `Copy of ${newCard.title}` + if (asTemplate === newCard.isTemplate) { + newCard.title = `Copy of ${newCard.title}` + } else if (asTemplate) { + // Template from card + newCard.title = 'New card template' + } else { + // Card from template + newCard.title = '' + } + newCard.isTemplate = asTemplate await this.insertBlocks( newBlocks, description, @@ -501,12 +516,28 @@ class Mutator { return [newBlocks, newCard.id] } - async duplicateBoard(boardId: string, description = 'duplicate board', afterRedo?: (newBoardId: string) => Promise, beforeUndo?: () => Promise): Promise<[IBlock[], string]> { + async duplicateBoard( + boardId: string, + description = 'duplicate board', + asTemplate = false, + afterRedo?: (newBoardId: string) => Promise, + beforeUndo?: () => Promise, + ): Promise<[IBlock[], string]> { const blocks = await octoClient.getSubtree(boardId, 3) - const [newBlocks1, newBoard] = OctoUtils.duplicateBlockTree(blocks, boardId) + const [newBlocks1, newBoard] = OctoUtils.duplicateBlockTree(blocks, boardId) as [IBlock[], MutableBoard, Record] const newBlocks = newBlocks1.filter((o) => o.type !== 'comment') Utils.log(`duplicateBoard: duplicating ${newBlocks.length} blocks`) - newBoard.title = `Copy of ${newBoard.title}` + + if (asTemplate === newBoard.isTemplate) { + newBoard.title = `Copy of ${newBoard.title}` + } else if (asTemplate) { + // Template from board + newBoard.title = 'New board template' + } else { + // Board from template + newBoard.title = '' + } + newBoard.isTemplate = asTemplate await this.insertBlocks( newBlocks, description, diff --git a/webapp/src/viewModel/boardTree.ts b/webapp/src/viewModel/boardTree.ts index a285401d8..f291e3f59 100644 --- a/webapp/src/viewModel/boardTree.ts +++ b/webapp/src/viewModel/boardTree.ts @@ -32,7 +32,6 @@ interface BoardTree { orderedCards(): Card[] mutableCopy(): MutableBoardTree - templateCopy(): MutableBoardTree } class MutableBoardTree implements BoardTree { @@ -406,14 +405,6 @@ class MutableBoardTree implements BoardTree { boardTree.incrementalUpdate(this.rawBlocks) return boardTree } - - templateCopy(): MutableBoardTree { - const [newBlocks, newBoard] = OctoUtils.duplicateBlockTree(this.rawBlocks, this.board.id) - - const boardTree = new MutableBoardTree(newBoard.id) - boardTree.incrementalUpdate(newBlocks) - return boardTree - } } export {MutableBoardTree, BoardTree, Group as BoardTreeGroup} diff --git a/webapp/src/viewModel/cardTree.ts b/webapp/src/viewModel/cardTree.ts index 6659de128..a6919aace 100644 --- a/webapp/src/viewModel/cardTree.ts +++ b/webapp/src/viewModel/cardTree.ts @@ -12,7 +12,6 @@ interface CardTree { readonly contents: readonly IOrderedBlock[] mutableCopy(): MutableCardTree - templateCopy(): MutableCardTree } class MutableCardTree implements CardTree { @@ -56,14 +55,6 @@ class MutableCardTree implements CardTree { cardTree.incrementalUpdate(this.rawBlocks) return cardTree } - - templateCopy(): MutableCardTree { - const [newBlocks, newCard] = OctoUtils.duplicateBlockTree(this.rawBlocks, this.card.id) - - const cardTree = new MutableCardTree(newCard.id) - cardTree.incrementalUpdate(newBlocks) - return cardTree - } } export {MutableCardTree, CardTree} From 998cb36421137ea06307912542c858c68c715e37 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Wed, 18 Nov 2020 10:03:37 -0800 Subject: [PATCH 13/23] Fix import --- webapp/src/components/workspaceComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/workspaceComponent.tsx b/webapp/src/components/workspaceComponent.tsx index 6c5450de5..2f31e23dd 100644 --- a/webapp/src/components/workspaceComponent.tsx +++ b/webapp/src/components/workspaceComponent.tsx @@ -9,7 +9,7 @@ import {WorkspaceTree} from '../viewModel/workspaceTree' import BoardComponent from './boardComponent' import Sidebar from './sidebar' -import {TableComponent} from './tableComponent' +import TableComponent from './tableComponent' import './workspaceComponent.scss' type Props = { From 4f84ecb741935d9883b611322a19e1ab3208a454 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Wed, 18 Nov 2020 11:01:07 -0800 Subject: [PATCH 14/23] Preserve line breaks in markdown --- webapp/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/utils.ts b/webapp/src/utils.ts index 3a1e220cb..05386a6d1 100644 --- a/webapp/src/utils.ts +++ b/webapp/src/utils.ts @@ -49,7 +49,7 @@ class Utils { // HACKHACK: Somehow, marked doesn't encode angle brackets const renderer = new marked.Renderer() renderer.link = (href, title, contents) => `${contents}` - const html = marked(text.replace(/ Date: Wed, 18 Nov 2020 11:11:51 -0800 Subject: [PATCH 15/23] Board descriptions --- webapp/i18n/en.json | 2 + webapp/src/blocks/board.ts | 17 +++++++ webapp/src/components/viewTitle.scss | 6 ++- webapp/src/components/viewTitle.tsx | 69 ++++++++++++++++++++++------ webapp/src/mutator.ts | 16 +++++++ 5 files changed, 96 insertions(+), 14 deletions(-) diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index a314849ea..862e983ce 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -85,9 +85,11 @@ "ViewHeader.test-distribute-cards": "TEST: Distribute cards", "ViewHeader.test-randomize-icons": "TEST: Randomize icons", "ViewHeader.untitled": "Untitled", + "ViewTitle.hide-description": "hide description", "ViewTitle.pick-icon": "Pick Icon", "ViewTitle.random-icon": "Random", "ViewTitle.remove-icon": "Remove Icon", + "ViewTitle.show-description": "show description", "ViewTitle.untitled-board": "Untitled Board", "WorkspaceComponent.editing-board-template": "You're editing a board template" } \ No newline at end of file diff --git a/webapp/src/blocks/board.ts b/webapp/src/blocks/board.ts index 5f0da1476..1b1246f97 100644 --- a/webapp/src/blocks/board.ts +++ b/webapp/src/blocks/board.ts @@ -37,6 +37,8 @@ interface IMutablePropertyTemplate extends IPropertyTemplate { interface Board extends IBlock { readonly icon: string + readonly description: string + readonly showDescription: boolean readonly isTemplate: boolean readonly cardProperties: readonly IPropertyTemplate[] duplicate(): MutableBoard @@ -50,6 +52,20 @@ class MutableBoard extends MutableBlock { this.fields.icon = value } + get description(): string { + return this.fields.description as string + } + set description(value: string) { + this.fields.description = value + } + + get showDescription(): boolean { + return Boolean(this.fields.showDescription) + } + set showDescription(value: boolean) { + this.fields.showDescription = value + } + get isTemplate(): boolean { return Boolean(this.fields.isTemplate) } @@ -69,6 +85,7 @@ class MutableBoard extends MutableBlock { this.type = 'board' this.icon = block.fields?.icon || '' + this.description = block.fields?.description || '' if (block.fields?.cardProperties) { // Deep clone of card properties and their options this.cardProperties = block.fields.cardProperties.map((o: IPropertyTemplate) => { diff --git a/webapp/src/components/viewTitle.scss b/webapp/src/components/viewTitle.scss index 5f05d42b0..09b007d20 100644 --- a/webapp/src/components/viewTitle.scss +++ b/webapp/src/components/viewTitle.scss @@ -5,7 +5,7 @@ align-items: center; &.add-buttons { - flex-direction: column; + flex-direction: row; min-height: 30px; color:rgba(var(--main-fg), 0.4); width: 100%; @@ -28,4 +28,8 @@ margin-bottom: 0px; flex-grow: 1; } + + &.description > * { + flex-grow: 1; + } } diff --git a/webapp/src/components/viewTitle.tsx b/webapp/src/components/viewTitle.tsx index 0afb77e69..b9cc5d298 100644 --- a/webapp/src/components/viewTitle.tsx +++ b/webapp/src/components/viewTitle.tsx @@ -9,8 +9,11 @@ import mutator from '../mutator' import Button from '../widgets/buttons/button' import Editable from '../widgets/editable' import EmojiIcon from '../widgets/icons/emoji' +import HideIcon from '../widgets/icons/hide' +import ShowIcon from '../widgets/icons/show' import BlockIconSelector from './blockIconSelector' +import {MarkdownEditor} from './markdownEditor' import './viewTitle.scss' type Props = { @@ -38,19 +41,47 @@ class ViewTitle extends React.Component { return ( <> -
- +
+ {!board.icon && + + } + {board.showDescription && + + } + {!board.showDescription && + + }
@@ -66,6 +97,18 @@ class ViewTitle extends React.Component { onCancel={() => this.setState({title: this.props.board.title})} />
+ + {board.showDescription && +
+ { + mutator.changeDescription(board, text) + }} + /> +
+ } ) } diff --git a/webapp/src/mutator.ts b/webapp/src/mutator.ts index 6aabcbf3b..c36f48936 100644 --- a/webapp/src/mutator.ts +++ b/webapp/src/mutator.ts @@ -156,6 +156,22 @@ class Mutator { await this.updateBlock(newBlock, block, description) } + async changeDescription(block: IBlock, boardDescription: string, description = 'change description') { + const newBoard = new MutableBoard(block) + newBoard.description = boardDescription + await this.updateBlock(newBoard, block, description) + } + + async showDescription(board: Board, showDescription = true, description?: string) { + const newBoard = new MutableBoard(board) + newBoard.showDescription = showDescription + let actionDescription = description + if (!actionDescription) { + actionDescription = showDescription ? 'show description' : 'hide description' + } + await this.updateBlock(newBoard, board, actionDescription) + } + async changeOrder(block: IOrderedBlock, order: number, description = 'change order') { const newBlock = new MutableOrderedBlock(block) newBlock.order = order From cc065ef86974d2abff49327776c7ae9651a254f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 19 Nov 2020 17:51:39 +0100 Subject: [PATCH 16/23] Adding another error handling to the migrations --- server/services/store/sqlstore/migrate.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/services/store/sqlstore/migrate.go b/server/services/store/sqlstore/migrate.go index 3d3da480b..821320598 100644 --- a/server/services/store/sqlstore/migrate.go +++ b/server/services/store/sqlstore/migrate.go @@ -2,6 +2,7 @@ package sqlstore import ( "errors" + "os" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database" @@ -46,7 +47,7 @@ func (s *SQLStore) Migrate() error { } err = m.Up() - if err != nil && !errors.Is(err, migrate.ErrNoChange) { + if err != nil && !errors.Is(err, migrate.ErrNoChange) && !errors.Is(err, os.ErrNotExist) { return err } From f87c811fbc8880274300156d983a9fe226d6c577 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Thu, 19 Nov 2020 14:50:17 -0800 Subject: [PATCH 17/23] Very basic mobile support: viewport, padding, dialogs --- webapp/html-templates/page.ejs | 4 ++- webapp/src/components/dialog.scss | 34 +++++++++++++++++++---- webapp/src/components/dialog.tsx | 23 +++++++++------ webapp/src/components/sidebar.scss | 2 +- webapp/src/styles/main.scss | 18 ++++++++++-- webapp/src/widgets/buttons/iconButton.tsx | 2 +- webapp/src/widgets/icons/close.scss | 7 +++++ webapp/src/widgets/icons/close.tsx | 19 +++++++++++++ 8 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 webapp/src/widgets/icons/close.scss create mode 100644 webapp/src/widgets/icons/close.tsx diff --git a/webapp/html-templates/page.ejs b/webapp/html-templates/page.ejs index 8c9756838..d81d8839a 100644 --- a/webapp/html-templates/page.ejs +++ b/webapp/html-templates/page.ejs @@ -3,7 +3,9 @@ - <%= htmlWebpackPlugin.options.title %> + + + <%= htmlWebpackPlugin.options.title %> diff --git a/webapp/src/components/dialog.scss b/webapp/src/components/dialog.scss index 9e909f03b..cb8f34234 100644 --- a/webapp/src/components/dialog.scss +++ b/webapp/src/components/dialog.scss @@ -17,13 +17,24 @@ background-color: rgb(var(--main-bg)); border-radius: 3px; box-shadow: rgba(var(--main-fg), 0.1) 0px 0px 0px 1px, rgba(var(--main-fg), 0.1) 0px 2px 4px; - margin: 72px auto; padding: 0; - max-width: 975px; - height: calc(100% - 144px); overflow-x: hidden; overflow-y: auto; + + @media not screen and (max-width: 975px) { + margin: 72px auto; + max-width: 975px; + height: calc(100% - 144px); + } + @media screen and (max-width: 975px) { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + > .banner { background-color: rgba(230, 220, 192, 0.9); text-align: center; @@ -33,13 +44,26 @@ display: flex; flex-direction: row; height: 30px; - margin: 10px + margin: 10px; + + > .IconButton:first-child { + /* Hide close button on larger screens */ + @media not screen and (max-width: 975px) { + display: none; + } + } } > .content { display: flex; flex-direction: column; align-items: flex-start; - padding: 10px 126px 10px 126px; + + @media not screen and (max-width: 975px) { + padding: 10px 126px; + } + @media screen and (max-width: 975px) { + padding: 10px 10px; + } } > .content.fullwidth { padding: 10px 0 10px 0; diff --git a/webapp/src/components/dialog.tsx b/webapp/src/components/dialog.tsx index 85a26205c..4c5371177 100644 --- a/webapp/src/components/dialog.tsx +++ b/webapp/src/components/dialog.tsx @@ -2,10 +2,10 @@ // See LICENSE.txt for license information. import React from 'react' -import MenuWrapper from '../widgets/menuWrapper' -import OptionsIcon from '../widgets/icons/options' import IconButton from '../widgets/buttons/iconButton' - +import CloseIcon from '../widgets/icons/close' +import OptionsIcon from '../widgets/icons/options' +import MenuWrapper from '../widgets/menuWrapper' import './dialog.scss' type Props = { @@ -23,17 +23,13 @@ export default class Dialog extends React.PureComponent { document.removeEventListener('keydown', this.keydownHandler) } - private close(): void { - this.props.onClose() - } - private keydownHandler = (e: KeyboardEvent): void => { if (e.target !== document.body) { return } if (e.keyCode === 27) { - this.close() + this.closeClicked() e.stopPropagation() } } @@ -46,13 +42,18 @@ export default class Dialog extends React.PureComponent { className='Dialog dialog-back' onMouseDown={(e) => { if (e.target === e.currentTarget) { - this.close() + this.closeClicked() } }} >
{toolsMenu &&
+ } + title={'Close dialog'} + />
}/> @@ -64,4 +65,8 @@ export default class Dialog extends React.PureComponent {
) } + + private closeClicked = () => { + this.props.onClose() + } } diff --git a/webapp/src/components/sidebar.scss b/webapp/src/components/sidebar.scss index 9df4a3ed8..d2fdd0352 100644 --- a/webapp/src/components/sidebar.scss +++ b/webapp/src/components/sidebar.scss @@ -13,7 +13,7 @@ position: absolute; top: 0; left: 0; - z-index: 15; + z-index: 5; min-height: 0; height: 50px; width: 50px; diff --git a/webapp/src/styles/main.scss b/webapp/src/styles/main.scss index 0f1a5bab3..63160fd39 100644 --- a/webapp/src/styles/main.scss +++ b/webapp/src/styles/main.scss @@ -99,7 +99,11 @@ hr { flex-direction: column; overflow: scroll; - padding: 10px 95px 50px 95px; + padding: 10px 95px 50px 95px; + + @media screen and (max-width: 768px) { + padding: 10px 10px 50px 10px; + } } .dragover { @@ -189,7 +193,13 @@ hr { align-items: flex-start; width: 100%; - padding-right: 126px; + + @media not screen and (max-width: 768px) { + padding-right: 126px; + } + @media screen and (max-width: 768px) { + padding-right: 10px; + } > * { flex: 1 1 auto; @@ -207,5 +217,7 @@ hr { padding-top: 10px; padding-right: 10px; - width: 126px; + @media not screen and (max-width: 768px) { + width: 126px; + } } diff --git a/webapp/src/widgets/buttons/iconButton.tsx b/webapp/src/widgets/buttons/iconButton.tsx index 2c10cdef4..53d21b93f 100644 --- a/webapp/src/widgets/buttons/iconButton.tsx +++ b/webapp/src/widgets/buttons/iconButton.tsx @@ -10,7 +10,7 @@ type Props = { icon?: React.ReactNode } -export default class Button extends React.PureComponent { +export default class IconButton extends React.PureComponent { render(): JSX.Element { return (
+ + + + ) +} From 0d2311fdc5c9d8271a96606a5877072a0555fd55 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Fri, 20 Nov 2020 11:35:56 -0800 Subject: [PATCH 18/23] Fix Safari dialog flex-shrink --- webapp/src/components/dialog.scss | 3 +++ webapp/src/styles/main.scss | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/dialog.scss b/webapp/src/components/dialog.scss index cb8f34234..ac40fc6af 100644 --- a/webapp/src/components/dialog.scss +++ b/webapp/src/components/dialog.scss @@ -35,6 +35,9 @@ bottom: 0; } + > * { + flex-shrink: 0; + } > .banner { background-color: rgba(230, 220, 192, 0.9); text-align: center; diff --git a/webapp/src/styles/main.scss b/webapp/src/styles/main.scss index 63160fd39..f005db3ae 100644 --- a/webapp/src/styles/main.scss +++ b/webapp/src/styles/main.scss @@ -194,10 +194,10 @@ hr { width: 100%; - @media not screen and (max-width: 768px) { + @media not screen and (max-width: 975px) { padding-right: 126px; } - @media screen and (max-width: 768px) { + @media screen and (max-width: 975px) { padding-right: 10px; } @@ -217,7 +217,7 @@ hr { padding-top: 10px; padding-right: 10px; - @media not screen and (max-width: 768px) { + @media not screen and (max-width: 975px) { width: 126px; } } From 2b186e2362796718e603ba923e6d498a9478cc4f Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Fri, 20 Nov 2020 11:54:40 -0800 Subject: [PATCH 19/23] Auto-focus on card title only if its empty --- webapp/src/components/cardDetail.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webapp/src/components/cardDetail.tsx b/webapp/src/components/cardDetail.tsx index 347b7c933..fc294c007 100644 --- a/webapp/src/components/cardDetail.tsx +++ b/webapp/src/components/cardDetail.tsx @@ -44,7 +44,9 @@ class CardDetail extends React.Component { } componentDidMount(): void { - this.titleRef.current?.focus() + if (!this.state.title) { + this.titleRef.current?.focus() + } } constructor(props: Props) { From 6d145800a8a805bdc66f1e0a8d4cb7262489ec6a Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Tue, 24 Nov 2020 12:13:05 -0800 Subject: [PATCH 20/23] Fix main frame scrolling --- webapp/src/components/workspaceComponent.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/src/components/workspaceComponent.scss b/webapp/src/components/workspaceComponent.scss index 5fe358c2e..3a46b5c4a 100644 --- a/webapp/src/components/workspaceComponent.scss +++ b/webapp/src/components/workspaceComponent.scss @@ -8,6 +8,7 @@ flex: 1 1 auto; display: flex; flex-direction: column; + overflow: auto; > .banner { background-color: rgba(230, 220, 192, 0.9); From a529cdd6bbd6d64e491c945d05169f9a02b3d590 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Tue, 1 Dec 2020 11:09:03 -0800 Subject: [PATCH 21/23] Fit content images to narrow width --- webapp/src/styles/main.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/src/styles/main.scss b/webapp/src/styles/main.scss index f005db3ae..b65f89f11 100644 --- a/webapp/src/styles/main.scss +++ b/webapp/src/styles/main.scss @@ -177,6 +177,7 @@ hr { } .octo-block img { + width: calc(100% - 20px); max-width: 500px; max-height: 500px; margin: 5px 0; From 6b2990281038aac8e5d93b20fbc2b62e465c841e Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Tue, 1 Dec 2020 11:31:20 -0800 Subject: [PATCH 22/23] Jest: code coverage reporting --- webapp/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webapp/package.json b/webapp/package.json index 6344fd942..13be9f065 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -26,8 +26,10 @@ "jest": { "transform": { "^.+\\.tsx?$": "ts-jest" - } - }, + }, + "collectCoverage": true, + "collectCoverageFrom": ["src/**/*.{ts,tsx,js,jsx}"] + }, "devDependencies": { "@formatjs/cli": "^2.13.2", "@formatjs/ts-transformer": "^2.11.3", From ea1835ca53898e668f47ea0e5a4c4a0ead803a2a Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Tue, 1 Dec 2020 11:55:04 -0800 Subject: [PATCH 23/23] UndoManager test: checkpoint --- webapp/src/undoManager.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webapp/src/undoManager.test.ts b/webapp/src/undoManager.test.ts index b01934781..402477f29 100644 --- a/webapp/src/undoManager.test.ts +++ b/webapp/src/undoManager.test.ts @@ -7,6 +7,7 @@ import {Utils} from './utils' test('Basic undo/redo', async () => { expect(undoManager.canUndo).toBe(false) expect(undoManager.canRedo).toBe(false) + expect(undoManager.currentCheckpoint).toBe(0) const values: string[] = [] @@ -22,6 +23,7 @@ test('Basic undo/redo', async () => { expect(undoManager.canUndo).toBe(true) expect(undoManager.canRedo).toBe(false) + expect(undoManager.currentCheckpoint).toBeGreaterThan(0) expect(Utils.arraysEqual(values, ['a'])).toBe(true) expect(undoManager.undoDescription).toBe('test') expect(undoManager.redoDescription).toBe(undefined) @@ -41,6 +43,7 @@ test('Basic undo/redo', async () => { await undoManager.clear() expect(undoManager.canUndo).toBe(false) expect(undoManager.canRedo).toBe(false) + expect(undoManager.currentCheckpoint).toBe(0) expect(undoManager.undoDescription).toBe(undefined) expect(undoManager.redoDescription).toBe(undefined) })