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 } 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/i18n/en.json b/webapp/i18n/en.json index 028323185..862e983ce 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", @@ -73,8 +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.untitled-board": "Untitled Board" + "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/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", diff --git a/webapp/src/blocks/board.ts b/webapp/src/blocks/board.ts index 168dc9390..1b1246f97 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,11 @@ 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 } class MutableBoard extends MutableBlock { @@ -46,6 +52,27 @@ 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) + } + set isTemplate(value: boolean) { + this.fields.isTemplate = value + } + get cardProperties(): IMutablePropertyTemplate[] { return this.fields.cardProperties as IPropertyTemplate[] } @@ -58,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) => { @@ -72,6 +100,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/boardComponent.tsx b/webapp/src/components/boardComponent.tsx index 79d19d3f8..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' @@ -279,19 +277,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 +346,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 +422,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 @@ -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) @@ -511,9 +506,11 @@ class BoardComponent extends React.Component { } } card.properties = {...card.properties, ...propertiesThatMeetFilters} - card.icon = BlockIcons.shared.randomIcon() - await mutator.insertBlocks( - blocksToInsert, + if (!card.icon) { + card.icon = BlockIcons.shared.randomIcon() + } + await mutator.insertBlock( + card, 'add card', async () => { this.setState({shownCardId: card.id}) 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) { 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/dialog.scss b/webapp/src/components/dialog.scss index 9e909f03b..ac40fc6af 100644 --- a/webapp/src/components/dialog.scss +++ b/webapp/src/components/dialog.scss @@ -17,13 +17,27 @@ 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; + } + + > * { + flex-shrink: 0; + } > .banner { background-color: rgba(230, 220, 192, 0.9); text-align: center; @@ -33,13 +47,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/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/sidebar.scss b/webapp/src/components/sidebar.scss index 2e6490ad1..d2fdd0352 100644 --- a/webapp/src/components/sidebar.scss +++ b/webapp/src/components/sidebar.scss @@ -8,13 +8,12 @@ color: rgb(var(--sidebar-fg)); background-color: rgb(var(--sidebar-bg)); padding: 10px 0; - overflow-y: scroll; &.hidden { position: absolute; top: 0; left: 0; - z-index: 15; + z-index: 5; min-height: 0; height: 50px; width: 50px; @@ -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; @@ -37,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); @@ -82,7 +87,7 @@ } } - .IconButton { + >.IconButton { background-color: var(--sidebar-bg); &:hover { background-color: rgba(var(--sidebar-fg), 0.1); @@ -122,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 24a82bc85..ee26d1ded 100644 --- a/webapp/src/components/sidebar.tsx +++ b/webapp/src/components/sidebar.tsx @@ -12,13 +12,13 @@ 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' @@ -87,111 +87,175 @@ 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={() => { + this.duplicateBoard(board.id) + }} + /> + + { + this.addTemplateFromBoard(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)'})} +
+
+ ))} +
+ ) + }) + } -
+
- + + + + + + + + + + + + {workspaceTree.boardTemplates.map((boardTemplate) => { + let displayName = boardTemplate.title || intl.formatMessage({id: 'Sidebar.untitled', defaultMessage: 'Untitled'}) + if (boardTemplate.icon) { + displayName = `${boardTemplate.icon} ${displayName}` + } + return ( + { + this.addBoardFromTemplate(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') + }} + /> + + + } + /> + ) + })} + + + + + + +
@@ -268,7 +332,9 @@ class Sidebar extends React.Component { 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 @@ -288,6 +354,78 @@ 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 + + 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/tableComponent.tsx b/webapp/src/components/tableComponent.tsx index e183ab07d..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 = { @@ -127,13 +126,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 +214,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]) { @@ -246,7 +253,7 @@ class TableComponent extends React.Component { const tableRow = ( { 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 - card.icon = BlockIcons.shared.randomIcon() - await mutator.insertBlocks( - blocksToInsert, + if (!card.icon) { + card.icon = BlockIcons.shared.randomIcon() + } + await mutator.insertBlock( + card, 'add card', async () => { if (show) { @@ -375,4 +381,4 @@ class TableComponent extends React.Component { } } -export {TableComponent} +export default injectIntl(TableComponent) diff --git a/webapp/src/components/tableRow.tsx b/webapp/src/components/tableRow.tsx index fdbb5aac6..61e8fa015 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) } } @@ -51,7 +51,6 @@ class TableRow extends React.Component { return (
{/* Name / title */} diff --git a/webapp/src/components/viewHeader.tsx b/webapp/src/components/viewHeader.tsx index 5c65c6bb7..0086fcac5 100644 --- a/webapp/src/components/viewHeader.tsx +++ b/webapp/src/components/viewHeader.tsx @@ -35,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 @@ -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() @@ -397,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) }} 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/components/workspaceComponent.scss b/webapp/src/components/workspaceComponent.scss index 8a1ed13c8..3a46b5c4a 100644 --- a/webapp/src/components/workspaceComponent.scss +++ b/webapp/src/components/workspaceComponent.scss @@ -2,5 +2,18 @@ flex: 1 1 auto; display: flex; flex-direction: row; - overflow: auto; + overflow: auto; + + > .mainFrame { + flex: 1 1 auto; + display: flex; + flex-direction: column; + overflow: auto; + + > .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..2f31e23dd 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' @@ -8,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 = { @@ -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..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 @@ -484,42 +500,69 @@ 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, idMap] = 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`) - const newCardId = idMap[cardId] - const newCard = newBlocks.find((o) => o.id === newCardId)! - 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, 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]> { + 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, idMap] = 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`) - const newBoardId = idMap[boardId] - const newBoard = newBlocks.find((o) => o.id === newBoardId)! - 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, 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/styles/main.scss b/webapp/src/styles/main.scss index 0f1a5bab3..b65f89f11 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 { @@ -173,6 +177,7 @@ hr { } .octo-block img { + width: calc(100% - 20px); max-width: 500px; max-height: 500px; margin: 5px 0; @@ -189,7 +194,13 @@ hr { align-items: flex-start; width: 100%; - padding-right: 126px; + + @media not screen and (max-width: 975px) { + padding-right: 126px; + } + @media screen and (max-width: 975px) { + padding-right: 10px; + } > * { flex: 1 1 auto; @@ -207,5 +218,7 @@ hr { padding-top: 10px; padding-right: 10px; - width: 126px; + @media not screen and (max-width: 975px) { + width: 126px; + } } 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) }) 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(/ { - const copy = MutableBlock.duplicate(content) - copy.parentId = card.id - return copy as IOrderedBlock - }) - - const cardTree = new MutableCardTree(card.id) - cardTree.incrementalUpdate([card, ...contents]) - return cardTree - } } export {MutableCardTree, CardTree} 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/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 (
{ } 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 => { this.saveOnBlur = false - this.elementRef.current!.blur() + this.elementRef.current?.blur() this.saveOnBlur = true } diff --git a/webapp/src/widgets/icons/close.scss b/webapp/src/widgets/icons/close.scss new file mode 100644 index 000000000..bd4c18608 --- /dev/null +++ b/webapp/src/widgets/icons/close.scss @@ -0,0 +1,7 @@ +.CloseIcon { + stroke: rgb(var(--main-fg), 0.5); + stroke-width: 4px; + fill: none; + width: 24px; + height: 24px; +} diff --git a/webapp/src/widgets/icons/close.tsx b/webapp/src/widgets/icons/close.tsx new file mode 100644 index 000000000..a91a3e8a5 --- /dev/null +++ b/webapp/src/widgets/icons/close.tsx @@ -0,0 +1,19 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react' + +import './close.scss' + +export default function CloseIcon(): JSX.Element { + return ( + + + + + ) +} diff --git a/webapp/src/widgets/menu/menu.scss b/webapp/src/widgets/menu/menu.scss index 24e4aca46..96772e2b2 100644 --- a/webapp/src/widgets/menu/menu.scss +++ b/webapp/src/widgets/menu/menu.scss @@ -33,32 +33,34 @@ 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; + white-space: nowrap; margin-right: 20px; } .SubmenuTriangleIcon { fill: rgba(var(--main-fg), 0.7); } + .Icon { width: 16px; height: 16px; margin-right: 5px; } + + .IconButton .Icon { + margin-right: 0; + } } }