From f22527e650203f428070c3f1992a5d84cdae3587 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Thu, 17 Dec 2020 12:02:12 -0800 Subject: [PATCH] Readonly support in UI --- webapp/cypress/integration/createBoard.js | 35 +- webapp/src/components/blockIconSelector.scss | 2 +- webapp/src/components/blockIconSelector.tsx | 11 +- webapp/src/components/boardCard.tsx | 49 +- webapp/src/components/boardComponent.tsx | 155 +++-- webapp/src/components/cardDetail.tsx | 171 ++--- webapp/src/components/cardDialog.tsx | 4 +- webapp/src/components/contentBlock.tsx | 164 ++--- webapp/src/components/dialog.tsx | 29 +- webapp/src/components/editable.tsx | 15 +- webapp/src/components/markdownEditor.tsx | 5 +- webapp/src/components/newCardButton.tsx | 113 ++++ .../src/components/propertyValueElement.tsx | 2 +- webapp/src/components/tableComponent.scss | 2 + webapp/src/components/tableComponent.tsx | 141 +++-- webapp/src/components/tableRow.tsx | 4 +- webapp/src/components/viewHeader.tsx | 595 ++++++++---------- webapp/src/components/viewMenu.tsx | 42 +- webapp/src/components/viewTitle.tsx | 9 +- webapp/src/components/workspaceComponent.tsx | 19 +- webapp/src/pages/boardPage.tsx | 17 +- webapp/src/widgets/editable.tsx | 2 + webapp/src/widgets/menuWrapper.scss | 4 + webapp/src/widgets/menuWrapper.tsx | 16 +- 24 files changed, 921 insertions(+), 685 deletions(-) create mode 100644 webapp/src/components/newCardButton.tsx diff --git a/webapp/cypress/integration/createBoard.js b/webapp/cypress/integration/createBoard.js index 023769f4e..3cdd9f9b1 100644 --- a/webapp/cypress/integration/createBoard.js +++ b/webapp/cypress/integration/createBoard.js @@ -1,24 +1,31 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +/* eslint-disable max-nested-callbacks */ + /// describe('Create and delete board / card', () => { + const timestamp = new Date().toLocaleString(); + const boardTitle = `Test Board (${timestamp})`; + const cardTitle = `Test Card (${timestamp})`; + it('Can create and delete a board and card', () => { cy.visit('/'); cy.contains('+ Add Board').click({force: true}); cy.contains('Empty board').click({force: true}); cy.get('.BoardComponent').should('exist'); + }); - const timestamp = new Date().toLocaleString(); - + it('Can set the board title', () => { // Board title - const boardTitle = `Test Board (${timestamp})`; cy.get('.ViewTitle>.Editable.title'). type(boardTitle). type('{enter}'). should('have.value', boardTitle); + }); + it('Can rename the board view', () => { // Rename board view const boardViewTitle = `Test board (${timestamp})`; cy.get('.ViewHeader'). @@ -30,13 +37,16 @@ describe('Create and delete board / card', () => { cy.get('.ViewHeader'). contains('.octo-editable', boardViewTitle). should('exist'); + }); + it('Can create a card', () => { // Create card cy.get('.ViewHeader').contains('New').click(); cy.get('.CardDetail').should('exist'); + }); + it('Can set the card title', () => { // Card title - const cardTitle = `Test Card (${timestamp})`; cy.get('.CardDetail>.Editable.title'). type(cardTitle). type('{enter}'). @@ -44,7 +54,9 @@ describe('Create and delete board / card', () => { // Close card cy.get('.Dialog.dialog-back').click({force: true}); + }); + it('Can create a table view', () => { // Create table view // cy.intercept('POST', '/api/v1/blocks').as('insertBlocks'); cy.get('.ViewHeader').get('.DropdownIcon').first().parent().click(); @@ -59,7 +71,9 @@ describe('Create and delete board / card', () => { // Card should exist in table cy.get(`.TableRow [value='${cardTitle}']`).should('exist'); + }); + it('Can rename the table view', () => { // Rename table view const tableViewTitle = `Test table (${timestamp})`; cy.get('.ViewHeader'). @@ -71,11 +85,24 @@ describe('Create and delete board / card', () => { cy.get('.ViewHeader'). contains('.octo-editable', tableViewTitle). should('exist'); + }); + it('Can sort the table', () => { // Sort cy.get('.ViewHeader').contains('Sort').click(); cy.get('.ViewHeader').contains('Sort').parent().contains('Name').click(); + }); + it('Can view the readonly board', () => { + cy.url().then((url) => { + const readonlyUrl = url + '&r=1'; + cy.visit(readonlyUrl); + cy.get('.ViewTitle>.Editable.title').should('have.attr', 'readonly'); + cy.visit(url); + }); + }); + + it('Can delete the board', () => { // Delete board cy.get('.Sidebar .octo-sidebar-list'). contains(boardTitle).first(). diff --git a/webapp/src/components/blockIconSelector.scss b/webapp/src/components/blockIconSelector.scss index 75e9a4d1b..83c9eadae 100644 --- a/webapp/src/components/blockIconSelector.scss +++ b/webapp/src/components/blockIconSelector.scss @@ -7,7 +7,7 @@ border-radius: 5px; align-items: center; justify-content: center; - &:hover { + &:not(.readonly):hover { background-color: rgba(var(--main-fg), 0.1); } &.size-s { diff --git a/webapp/src/components/blockIconSelector.tsx b/webapp/src/components/blockIconSelector.tsx index 586bcf301..798c8bb0d 100644 --- a/webapp/src/components/blockIconSelector.tsx +++ b/webapp/src/components/blockIconSelector.tsx @@ -18,6 +18,7 @@ type Props = { block: Board|Card size?: 's' | 'm' | 'l' intl: IntlShape + readonly?: boolean } class BlockIconSelector extends React.Component { @@ -41,10 +42,17 @@ class BlockIconSelector extends React.Component { if (!block.icon) { return null } + let className = `octo-icon size-${size}` + if (this.props.readonly) { + className += ' readonly' + } + const iconElement =
{block.icon}
return (
+ {this.props.readonly && iconElement} + {!this.props.readonly && -
{block.icon}
+ {iconElement} { />
+ }
) } diff --git a/webapp/src/components/boardCard.tsx b/webapp/src/components/boardCard.tsx index 98dcd3b02..87cf9d37b 100644 --- a/webapp/src/components/boardCard.tsx +++ b/webapp/src/components/boardCard.tsx @@ -26,6 +26,7 @@ type BoardCardProps = { onDragEnd: (e: React.DragEvent) => void onDrop: (e: React.DragEvent) => void intl: IntlShape + readonly: boolean } type BoardCardState = { @@ -54,7 +55,7 @@ class BoardCard extends React.Component { const element = (
{ @@ -86,28 +87,30 @@ class BoardCard extends React.Component { } }} > - - }/> - - } - id='delete' - name={intl.formatMessage({id: 'BoardCard.delete', defaultMessage: 'Delete'})} - onClick={() => mutator.deleteBlock(card, 'delete card')} - /> - } - id='duplicate' - name={intl.formatMessage({id: 'BoardCard.duplicate', defaultMessage: 'Duplicate'})} - onClick={() => { - mutator.duplicateCard(card.id) - }} - /> - - + {!this.props.readonly && + + }/> + + } + id='delete' + name={intl.formatMessage({id: 'BoardCard.delete', defaultMessage: 'Delete'})} + onClick={() => mutator.deleteBlock(card, 'delete card')} + /> + } + id='duplicate' + name={intl.formatMessage({id: 'BoardCard.duplicate', defaultMessage: 'Duplicate'})} + onClick={() => { + mutator.duplicateCard(card.id) + }} + /> + + + }
{ card.icon ?
{card.icon}
: undefined } diff --git a/webapp/src/components/boardComponent.tsx b/webapp/src/components/boardComponent.tsx index def1ae490..6bb3c42f6 100644 --- a/webapp/src/components/boardComponent.tsx +++ b/webapp/src/components/boardComponent.tsx @@ -36,6 +36,7 @@ type Props = { showView: (id: string) => void setSearchText: (text?: string) => void intl: IntlShape + readonly: boolean } type State = { @@ -140,6 +141,7 @@ class BoardComponent extends React.Component { cardId={this.state.shownCardId} onClose={() => this.showCard(undefined)} showCard={(cardId) => this.showCard(cardId)} + readonly={this.props.readonly} /> } @@ -147,6 +149,7 @@ class BoardComponent extends React.Component {
@@ -159,6 +162,7 @@ class BoardComponent extends React.Component { addCardTemplate={this.addCardTemplate} editCardTemplate={this.editCardTemplate} withGroupBy={true} + readonly={this.props.readonly} />
{ id='BoardComponent.hidden-columns' defaultMessage='Hidden columns' /> -
} +
+ } -
- -
+ {!this.props.readonly && +
+ +
+ }
{/* Main content */} @@ -205,16 +212,18 @@ class BoardComponent extends React.Component { onDrop={() => this.onDropToColumn(group.option)} > {group.cards.map((card) => this.renderCard(card, visiblePropertyTemplates))} - + {!this.props.readonly && + + } ))} @@ -240,6 +249,7 @@ class BoardComponent extends React.Component { card={card} visiblePropertyTemplates={visiblePropertyTemplates} key={card.id} + readonly={this.props.readonly} isSelected={this.state.selectedCardIds.includes(card.id)} onClick={(e) => { this.cardClicked(e, card) @@ -277,7 +287,7 @@ class BoardComponent extends React.Component { ref={ref} className='octo-board-header-cell' - draggable={true} + draggable={!this.props.readonly} onDragStart={() => { this.draggedHeaderOption = group.option }} @@ -320,21 +330,25 @@ class BoardComponent extends React.Component {
- - }/> - - } - name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})} - onClick={() => mutator.hideViewColumn(activeView, '')} + {!this.props.readonly && + <> + + }/> + + } + name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})} + onClick={() => mutator.hideViewColumn(activeView, '')} + /> + + + } + onClick={() => this.addCard(undefined)} /> - - - } - onClick={() => this.addCard(undefined)} - /> + + }
) } @@ -346,7 +360,7 @@ class BoardComponent extends React.Component { ref={ref} className='octo-board-header-cell' - draggable={true} + draggable={!this.props.readonly} onDragStart={() => { this.draggedHeaderOption = group.option }} @@ -380,39 +394,44 @@ class BoardComponent extends React.Component { onChanged={(text) => { this.propertyNameChanged(group.option, text) }} + readonly={this.props.readonly} />
- - }/> - - } - name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})} - onClick={() => mutator.hideViewColumn(activeView, group.option.id)} + {!this.props.readonly && + <> + + }/> + + } + name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})} + onClick={() => mutator.hideViewColumn(activeView, group.option.id)} + /> + } + name={intl.formatMessage({id: 'BoardComponent.delete', defaultMessage: 'Delete'})} + onClick={() => mutator.deletePropertyOption(boardTree, boardTree.groupByProperty!, group.option)} + /> + + {Constants.menuColors.map((color) => ( + mutator.changePropertyOptionColor(boardTree.board, boardTree.groupByProperty!, group.option, color.id)} + /> + ))} + + + } + onClick={() => this.addCard(group.option.id)} /> - } - name={intl.formatMessage({id: 'BoardComponent.delete', defaultMessage: 'Delete'})} - onClick={() => mutator.deletePropertyOption(boardTree, boardTree.groupByProperty!, group.option)} - /> - - {Constants.menuColors.map((color) => ( - mutator.changePropertyOptionColor(boardTree.board, boardTree.groupByProperty!, group.option, color.id)} - /> - ))} - - - } - onClick={() => this.addCard(group.option.id)} - /> + + }
) } @@ -457,7 +476,9 @@ class BoardComponent extends React.Component { this.onDropToColumn(group.option) }} > - +
{ block={block} card={card} contents={cardTree.contents} + readonly={this.props.readonly} /> ))}
) @@ -81,20 +83,22 @@ class CardDetail extends React.Component { contentElements = (
- { - if (text) { - const block = new MutableTextBlock() - block.parentId = card.id - block.rootId = card.rootId - block.title = text - block.order = (this.props.cardTree.contents.length + 1) * 1000 - mutator.insertBlock(block, 'add card text') - } - }} - /> + {!this.props.readonly && + { + if (text) { + const block = new MutableTextBlock() + block.parentId = card.id + block.rootId = card.rootId + block.title = text + block.order = (this.props.cardTree.contents.length + 1) * 1000 + mutator.insertBlock(block, 'add card text') + } + }} + /> + }
) } @@ -107,8 +111,9 @@ class CardDetail extends React.Component { - {!icon && + {!this.props.readonly && !icon &&
- mutator.renameProperty(board, propertyTemplate.id, newName)} - onTypeChanged={(newType: PropertyType) => mutator.changePropertyType(boardTree, propertyTemplate, newType)} - onDelete={(id: string) => mutator.deleteProperty(boardTree, id)} - /> - + {this.props.readonly &&
{propertyTemplate.name}
} + {!this.props.readonly && + +
+ mutator.renameProperty(board, propertyTemplate.id, newName)} + onTypeChanged={(newType: PropertyType) => mutator.changePropertyType(boardTree, propertyTemplate, newType)} + onDelete={(id: string) => mutator.deleteProperty(boardTree, id)} + /> +
+ } { ) })} -
- -
+ {!this.props.readonly && +
+ +
+ }
{/* Comments */} -
- -
+ {!this.props.readonly && + <> +
+ +
+ + } {/* Content blocks */} @@ -201,38 +216,40 @@ class CardDetail extends React.Component { {contentElements} -
- - - - { - const block = new MutableTextBlock() - block.parentId = card.id - block.rootId = card.rootId - block.order = (this.props.cardTree.contents.length + 1) * 1000 - mutator.insertBlock(block, 'add text') - }} - /> - Utils.selectLocalFile( - (file) => mutator.createImageBlock(card, file, (this.props.cardTree.contents.length + 1) * 1000), - '.jpg,.jpeg,.png', - )} - /> + {!this.props.readonly && +
+ + + + { + const block = new MutableTextBlock() + block.parentId = card.id + block.rootId = card.rootId + block.order = (this.props.cardTree.contents.length + 1) * 1000 + mutator.insertBlock(block, 'add text') + }} + /> + Utils.selectLocalFile( + (file) => mutator.createImageBlock(card, file, (this.props.cardTree.contents.length + 1) * 1000), + '.jpg,.jpeg,.png', + )} + /> - - -
+
+
+
+ } ) } diff --git a/webapp/src/components/cardDialog.tsx b/webapp/src/components/cardDialog.tsx index 18f2145f0..3b67dcdd3 100644 --- a/webapp/src/components/cardDialog.tsx +++ b/webapp/src/components/cardDialog.tsx @@ -20,6 +20,7 @@ type Props = { onClose: () => void showCard: (cardId?: string) => void intl: IntlShape + readonly: boolean } type State = { @@ -106,7 +107,7 @@ class CardDialog extends React.Component { return ( {(cardTree?.card.isTemplate) &&
@@ -120,6 +121,7 @@ class CardDialog extends React.Component { } {(!this.state.cardTree && this.state.syncComplete) && diff --git a/webapp/src/components/contentBlock.tsx b/webapp/src/components/contentBlock.tsx index 29619f16f..91ae675e6 100644 --- a/webapp/src/components/contentBlock.tsx +++ b/webapp/src/components/contentBlock.tsx @@ -29,6 +29,7 @@ type Props = { block: IOrderedBlock card: IBlock contents: readonly IOrderedBlock[] + readonly: boolean } class ContentBlock extends React.PureComponent { @@ -44,89 +45,91 @@ class ContentBlock extends React.PureComponent { return (
- - }/> - - {index > 0 && - } - onClick={() => { - const previousBlock = contents[index - 1] - const newOrder = OctoUtils.getOrderBefore(previousBlock, contents) - Utils.log(`moveUp ${newOrder}`) - mutator.changeOrder(block, newOrder, 'move up') - }} - />} - {index < (contents.length - 1) && - } - onClick={() => { - const nextBlock = contents[index + 1] - const newOrder = OctoUtils.getOrderAfter(nextBlock, contents) - Utils.log(`moveDown ${newOrder}`) - mutator.changeOrder(block, newOrder, 'move down') - }} - />} - } - > - } - onClick={() => { - const newBlock = new MutableTextBlock() - newBlock.parentId = card.id - newBlock.rootId = card.rootId + {!this.props.readonly && + + }/> + + {index > 0 && + } + onClick={() => { + const previousBlock = contents[index - 1] + const newOrder = OctoUtils.getOrderBefore(previousBlock, contents) + Utils.log(`moveUp ${newOrder}`) + mutator.changeOrder(block, newOrder, 'move up') + }} + />} + {index < (contents.length - 1) && + } + onClick={() => { + const nextBlock = contents[index + 1] + const newOrder = OctoUtils.getOrderAfter(nextBlock, contents) + Utils.log(`moveDown ${newOrder}`) + mutator.changeOrder(block, newOrder, 'move down') + }} + />} + } + > + } + onClick={() => { + const newBlock = new MutableTextBlock() + newBlock.parentId = card.id + newBlock.rootId = card.rootId - // TODO: Handle need to reorder all blocks - newBlock.order = OctoUtils.getOrderBefore(block, contents) - Utils.log(`insert block ${block.id}, order: ${block.order}`) - mutator.insertBlock(newBlock, 'insert card text') - }} - /> - } - onClick={() => { - Utils.selectLocalFile( - (file) => { - mutator.createImageBlock(card, file, OctoUtils.getOrderBefore(block, contents)) - }, - '.jpg,.jpeg,.png') - }} - /> - } - onClick={() => { - const newBlock = new MutableDividerBlock() - newBlock.parentId = card.id - newBlock.rootId = card.rootId + // TODO: Handle need to reorder all blocks + newBlock.order = OctoUtils.getOrderBefore(block, contents) + Utils.log(`insert block ${block.id}, order: ${block.order}`) + mutator.insertBlock(newBlock, 'insert card text') + }} + /> + } + onClick={() => { + Utils.selectLocalFile( + (file) => { + mutator.createImageBlock(card, file, OctoUtils.getOrderBefore(block, contents)) + }, + '.jpg,.jpeg,.png') + }} + /> + } + onClick={() => { + const newBlock = new MutableDividerBlock() + newBlock.parentId = card.id + newBlock.rootId = card.rootId - // TODO: Handle need to reorder all blocks - newBlock.order = OctoUtils.getOrderBefore(block, contents) - Utils.log(`insert block ${block.id}, order: ${block.order}`) - mutator.insertBlock(newBlock, 'insert card text') - }} + // TODO: Handle need to reorder all blocks + newBlock.order = OctoUtils.getOrderBefore(block, contents) + Utils.log(`insert block ${block.id}, order: ${block.order}`) + mutator.insertBlock(newBlock, 'insert card text') + }} + /> + + } + id='delete' + name='Delete' + onClick={() => mutator.deleteBlock(block)} /> - - } - id='delete' - name='Delete' - onClick={() => mutator.deleteBlock(block)} - /> - - + + + }
{block.type === 'text' && { Utils.log(`change text ${block.id}, ${text}`) mutator.changeTitle(block, text, 'edit card text') }} + readonly={this.props.readonly} />} {block.type === 'divider' &&
} {block.type === 'image' && diff --git a/webapp/src/components/dialog.tsx b/webapp/src/components/dialog.tsx index b90ae0008..fefba4056 100644 --- a/webapp/src/components/dialog.tsx +++ b/webapp/src/components/dialog.tsx @@ -47,20 +47,23 @@ export default class Dialog extends React.PureComponent { }} >
- {toolsMenu &&
- } - title={'Close dialog'} - className='hideOnWidescreen' - /> -
- - }/> - {toolsMenu} - -
} + {toolsMenu && + <> + } + title={'Close dialog'} + className='hideOnWidescreen' + /> +
+ + }/> + {toolsMenu} + + + } +
{this.props.children}
diff --git a/webapp/src/components/editable.tsx b/webapp/src/components/editable.tsx index eecd7d86d..4ec24463f 100644 --- a/webapp/src/components/editable.tsx +++ b/webapp/src/components/editable.tsx @@ -13,6 +13,7 @@ type Props = { isMarkdown: boolean isMultiline: boolean allowEmpty: boolean + readonly?: boolean onFocus?: () => void onBlur?: () => void @@ -92,7 +93,7 @@ class Editable extends React.PureComponent {
{ dangerouslySetInnerHTML={{__html: html}} onFocus={() => { + if (this.props.readonly) { + return + } + if (this.elementRef.current) { this.elementRef.current.innerText = this.text this.elementRef.current.style.color = style?.color || '' @@ -112,6 +117,10 @@ class Editable extends React.PureComponent { }} onBlur={async () => { + if (this.props.readonly) { + return + } + if (this.elementRef.current) { const newText = this.elementRef.current.innerText const oldText = this.props.text || '' @@ -134,6 +143,10 @@ class Editable extends React.PureComponent { }} onKeyDown={(e) => { + if (this.props.readonly) { + return + } + if (e.keyCode === 27 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // ESC e.stopPropagation() this.elementRef.current?.blur() diff --git a/webapp/src/components/markdownEditor.tsx b/webapp/src/components/markdownEditor.tsx index f01f1ed06..8b4ec6ba0 100644 --- a/webapp/src/components/markdownEditor.tsx +++ b/webapp/src/components/markdownEditor.tsx @@ -12,6 +12,7 @@ type Props = { placeholderText?: string uniqueId?: string className?: string + readonly?: boolean onChange?: (text: string) => void onFocus?: () => void @@ -28,7 +29,7 @@ class MarkdownEditor extends React.Component { } get text(): string { - return this.elementRef.current!.state.value + return this.elementRef.current!.state.value || '' } set text(value: string) { this.elementRef.current!.setState({value}) @@ -91,7 +92,7 @@ class MarkdownEditor extends React.Component { style={{display: this.state.isEditing ? 'none' : undefined}} dangerouslySetInnerHTML={{__html: html}} onClick={() => { - if (!this.state.isEditing) { + if (!this.props.readonly && !this.state.isEditing) { this.showEditor() } }} diff --git a/webapp/src/components/newCardButton.tsx b/webapp/src/components/newCardButton.tsx new file mode 100644 index 000000000..7a917b609 --- /dev/null +++ b/webapp/src/components/newCardButton.tsx @@ -0,0 +1,113 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react' +import {FormattedMessage, injectIntl, IntlShape} from 'react-intl' + +import mutator from '../mutator' +import {BoardTree} from '../viewModel/boardTree' +import ButtonWithMenu from '../widgets/buttons/buttonWithMenu' +import IconButton from '../widgets/buttons/iconButton' +import CardIcon from '../widgets/icons/card' +import DeleteIcon from '../widgets/icons/delete' +import OptionsIcon from '../widgets/icons/options' +import Menu from '../widgets/menu' +import MenuWrapper from '../widgets/menuWrapper' + +type Props = { + boardTree: BoardTree + addCard: () => void + addCardFromTemplate: (cardTemplateId: string) => void + addCardTemplate: () => void + editCardTemplate: (cardTemplateId: string) => void + intl: IntlShape +} + +class NewCardButton extends React.PureComponent { + render(): JSX.Element { + const {intl, boardTree} = this.props + + return ( + { + this.props.addCard() + }} + text={( + + )} + > + + {boardTree.cardTemplates.length > 0 && <> + + + + + + + + } + + {boardTree.cardTemplates.map((cardTemplate) => { + const displayName = cardTemplate.title || intl.formatMessage({id: 'ViewHeader.untitled', defaultMessage: 'Untitled'}) + return ( + {cardTemplate.icon}
} + onClick={() => { + this.props.addCardFromTemplate(cardTemplate.id) + }} + rightIcon={ + + }/> + + { + this.props.editCardTemplate(cardTemplate.id) + }} + /> + } + id='delete' + name={intl.formatMessage({id: 'ViewHeader.delete-template', defaultMessage: 'Delete'})} + onClick={async () => { + await mutator.deleteBlock(cardTemplate, 'delete card template') + }} + /> + + + } + /> + ) + })} + + } + onClick={() => { + this.props.addCard() + }} + /> + + this.props.addCardTemplate()} + /> + + + ) + } +} + +export default injectIntl(NewCardButton) diff --git a/webapp/src/components/propertyValueElement.tsx b/webapp/src/components/propertyValueElement.tsx index 227acee26..d3a83fa74 100644 --- a/webapp/src/components/propertyValueElement.tsx +++ b/webapp/src/components/propertyValueElement.tsx @@ -50,7 +50,7 @@ export default class PropertyValueElement extends React.Component } if (propertyTemplate.type === 'select') { - let className = 'octo-propertyvalue' + let className = 'octo-propertyvalue octo-label' if (!displayValue) { className += ' empty' } diff --git a/webapp/src/components/tableComponent.scss b/webapp/src/components/tableComponent.scss index 144a8733e..6f228e2ea 100644 --- a/webapp/src/components/tableComponent.scss +++ b/webapp/src/components/tableComponent.scss @@ -55,6 +55,8 @@ .octo-propertyvalue { line-height: 17px; + overflow: hidden; + text-overflow: ellipsis; } .octo-editable, diff --git a/webapp/src/components/tableComponent.tsx b/webapp/src/components/tableComponent.tsx index e30ec4131..c914ef3f8 100644 --- a/webapp/src/components/tableComponent.tsx +++ b/webapp/src/components/tableComponent.tsx @@ -29,6 +29,7 @@ type Props = { showView: (id: string) => void setSearchText: (text?: string) => void intl: IntlShape + readonly: boolean } type State = { @@ -80,12 +81,14 @@ class TableComponent extends React.Component { cardId={this.state.shownCardId} onClose={() => this.showCard(undefined)} showCard={(cardId) => this.showCard(cardId)} + readonly={this.props.readonly} /> }
@@ -97,6 +100,7 @@ class TableComponent extends React.Component { addCardFromTemplate={this.addCardFromTemplate} addCardTemplate={this.addCardTemplate} editCardTemplate={this.editCardTemplate} + readonly={this.props.readonly} /> {/* Main content */} @@ -115,10 +119,11 @@ class TableComponent extends React.Component { className='octo-table-cell title-cell header-cell' style={{overflow: 'unset', width: this.columnWidth(Constants.titleColumnId)}} > - +
{
- { - const originalWidth = this.columnWidth(Constants.titleColumnId) - const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset) - 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) - if (titleRef.current) { - titleRef.current.style.width = `${newWidth}px` - } + {!this.props.readonly && + { + const originalWidth = this.columnWidth(Constants.titleColumnId) + const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset) + 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) + if (titleRef.current) { + titleRef.current.style.width = `${newWidth}px` + } - const columnWidths = {...activeView.columnWidths} - if (newWidth !== columnWidths[Constants.titleColumnId]) { - columnWidths[Constants.titleColumnId] = newWidth + const columnWidths = {...activeView.columnWidths} + if (newWidth !== columnWidths[Constants.titleColumnId]) { + columnWidths[Constants.titleColumnId] = newWidth - const newView = new MutableBoardView(activeView) - newView.columnWidths = columnWidths - mutator.updateBlock(newView, activeView, 'resize column') - } - }} - /> + const newView = new MutableBoardView(activeView) + newView.columnWidths = columnWidths + mutator.updateBlock(newView, activeView, 'resize column') + } + }} + /> + }
{/* Table header row */} @@ -199,11 +206,12 @@ class TableComponent extends React.Component { this.onDropToColumn(template) }} > - +
{ this.draggedHeaderTemplate = template }} @@ -222,32 +230,34 @@ class TableComponent extends React.Component {
- { - const originalWidth = this.columnWidth(template.id) - const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset) - 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) - if (headerRef.current) { - headerRef.current.style.width = `${newWidth}px` - } + {!this.props.readonly && + { + const originalWidth = this.columnWidth(template.id) + const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset) + 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) + if (headerRef.current) { + headerRef.current.style.width = `${newWidth}px` + } - const columnWidths = {...activeView.columnWidths} - if (newWidth !== columnWidths[template.id]) { - columnWidths[template.id] = newWidth + const columnWidths = {...activeView.columnWidths} + if (newWidth !== columnWidths[template.id]) { + columnWidths[template.id] = newWidth - const newView = new MutableBoardView(activeView) - newView.columnWidths = columnWidths - mutator.updateBlock(newView, activeView, 'resize column') - } - }} - /> + const newView = new MutableBoardView(activeView) + newView.columnWidths = columnWidths + mutator.updateBlock(newView, activeView, 'resize column') + } + }} + /> + }
) })}
@@ -276,6 +286,7 @@ class TableComponent extends React.Component { } }} showCard={this.showCard} + readonly={this.props.readonly} />) this.cardIdToRowMap.set(card.id, tableRowRef) @@ -286,17 +297,19 @@ class TableComponent extends React.Component { {/* Add New row */}
-
{ - this.addCard() - }} - > - -
+ {!this.props.readonly && +
{ + this.addCard() + }} + > + +
+ }
diff --git a/webapp/src/components/tableRow.tsx b/webapp/src/components/tableRow.tsx index 61e8fa015..852169d41 100644 --- a/webapp/src/components/tableRow.tsx +++ b/webapp/src/components/tableRow.tsx @@ -19,6 +19,7 @@ type Props = { focusOnMount: boolean onSaveWithEnter: () => void showCard: (cardId: string) => void + readonly: boolean } type State = { @@ -74,6 +75,7 @@ class TableRow extends React.Component { } }} onCancel={() => this.setState({title: card.title})} + readonly={this.props.readonly} />
@@ -99,7 +101,7 @@ class TableRow extends React.Component { style={{width: this.columnWidth(template.id)}} > void withGroupBy?: boolean intl: IntlShape + readonly: boolean } type State = { @@ -66,6 +65,282 @@ class ViewHeader extends React.Component { } } + render(): JSX.Element { + const {boardTree, showView, withGroupBy, intl} = this.props + const {board, activeView} = boardTree + + const hasFilter = activeView.filter && activeView.filter.filters?.length > 0 + const hasSort = activeView.sortOptions.length > 0 + + return ( +
+ { + mutator.changeTitle(activeView, text) + }} + readonly={this.props.readonly} + /> + + }/> + + + +
+ + {!this.props.readonly && + <> + {/* Card properties */} + + + + + {boardTree.board.cardProperties.map((option: IPropertyTemplate) => ( + { + let newVisiblePropertyIds = [] + if (activeView.visiblePropertyIds.includes(propertyId)) { + newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o: string) => o !== propertyId) + } else { + newVisiblePropertyIds = [...activeView.visiblePropertyIds, propertyId] + } + mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds) + }} + /> + ))} + + + + {/* Group by */} + + {withGroupBy && + + + + {boardTree.board.cardProperties.filter((o: IPropertyTemplate) => o.type === 'select').map((option: IPropertyTemplate) => ( + : undefined} + onClick={(id) => { + if (boardTree.activeView.groupById === id) { + return + } + + mutator.changeViewGroupById(boardTree.activeView, id) + }} + /> + ))} + + } + + {/* Filter */} + +
+ + {this.state.showFilter && + } +
+ + {/* Sort */} + + + + + {(activeView.sortOptions.length > 0) && + <> + { + // This sets the manual card order to the currently displayed order + // Note: Perform this as a single update to change both properties correctly + const newView = new MutableBoardView(activeView) + newView.cardOrder = boardTree.orderedCards().map((o) => o.id) + newView.sortOptions = [] + mutator.updateBlock(newView, activeView, 'reorder') + }} + /> + + { + mutator.changeViewSortOptions(activeView, []) + }} + /> + + + + } + + {this.sortDisplayOptions().map((option) => { + let rightIcon: JSX.Element | undefined + if (activeView.sortOptions.length > 0) { + const sortOption = activeView.sortOptions[0] + if (sortOption.propertyId === option.id) { + rightIcon = sortOption.reversed ? : + } + } + return ( + { + let newSortOptions: ISortOption[] = [] + if (activeView.sortOptions[0] && activeView.sortOptions[0].propertyId === propertyId) { + // Already sorting by name, so reverse it + newSortOptions = [ + {propertyId, reversed: !activeView.sortOptions[0].reversed}, + ] + } else { + newSortOptions = [ + {propertyId, reversed: false}, + ] + } + mutator.changeViewSortOptions(activeView, newSortOptions) + }} + /> + ) + })} + + + + } + + {/* Search */} + + {this.state.isSearching && + { + this.searchChanged(text) + }} + onKeyDown={(e) => { + this.onSearchKeyDown(e) + }} + /> + } + + {!this.state.isSearching && + } + + {/* Options menu */} + + {!this.props.readonly && + <> + + }/> + + CsvExporter.exportTableCsv(boardTree)} + /> + Archiver.exportBoardTree(boardTree)} + /> + + + + this.testAddCards(100)} + /> + this.testAddCards(1000)} + /> + this.testDistributeCards()} + /> + this.testRandomizeIcons()} + /> + + + + {/* New card button */} + + + + } +
+ ) + } + private showFilterDialog = () => { this.setState({showFilter: true}) } @@ -145,320 +420,6 @@ class ViewHeader extends React.Component { }) } - render(): JSX.Element { - const {boardTree, showView, withGroupBy, intl} = this.props - const {board, activeView} = boardTree - - const hasFilter = activeView.filter && activeView.filter.filters?.length > 0 - const hasSort = activeView.sortOptions.length > 0 - - return ( -
- { - mutator.changeTitle(activeView, text) - }} - /> - - }/> - - -
- - - - {boardTree.board.cardProperties.map((option: IPropertyTemplate) => ( - { - let newVisiblePropertyIds = [] - if (activeView.visiblePropertyIds.includes(propertyId)) { - newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o: string) => o !== propertyId) - } else { - newVisiblePropertyIds = [...activeView.visiblePropertyIds, propertyId] - } - mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds) - }} - /> - ))} - - - {withGroupBy && - - - - {boardTree.board.cardProperties.filter((o: IPropertyTemplate) => o.type === 'select').map((option: IPropertyTemplate) => ( - : undefined} - onClick={(id) => { - if (boardTree.activeView.groupById === id) { - return - } - - mutator.changeViewGroupById(boardTree.activeView, id) - }} - /> - ))} - - } -
- - {this.state.showFilter && - } -
- - - - {(activeView.sortOptions.length > 0) && - <> - { - // This sets the manual card order to the currently displayed order - // Note: Perform this as a single update to change both properties correctly - const newView = new MutableBoardView(activeView) - newView.cardOrder = boardTree.orderedCards().map((o) => o.id) - newView.sortOptions = [] - mutator.updateBlock(newView, activeView, 'reorder') - }} - /> - - { - mutator.changeViewSortOptions(activeView, []) - }} - /> - - - - } - - {this.sortDisplayOptions().map((option) => { - let rightIcon: JSX.Element | undefined - if (activeView.sortOptions.length > 0) { - const sortOption = activeView.sortOptions[0] - if (sortOption.propertyId === option.id) { - rightIcon = sortOption.reversed ? : - } - } - return ( - { - let newSortOptions: ISortOption[] = [] - if (activeView.sortOptions[0] && activeView.sortOptions[0].propertyId === propertyId) { - // Already sorting by name, so reverse it - newSortOptions = [ - {propertyId, reversed: !activeView.sortOptions[0].reversed}, - ] - } else { - newSortOptions = [ - {propertyId, reversed: false}, - ] - } - mutator.changeViewSortOptions(activeView, newSortOptions) - }} - /> - ) - })} - - - {this.state.isSearching && - { - this.searchChanged(text) - }} - onKeyDown={(e) => { - this.onSearchKeyDown(e) - }} - />} - {!this.state.isSearching && - } - - }/> - - CsvExporter.exportTableCsv(boardTree)} - /> - Archiver.exportBoardTree(boardTree)} - /> - - - - this.testAddCards(100)} - /> - this.testAddCards(1000)} - /> - this.testDistributeCards()} - /> - this.testRandomizeIcons()} - /> - - - - { - this.props.addCard() - }} - text={( - - )} - > - - {boardTree.cardTemplates.length > 0 && <> - - - - - - - - } - - {boardTree.cardTemplates.map((cardTemplate) => { - const displayName = cardTemplate.title || intl.formatMessage({id: 'ViewHeader.untitled', defaultMessage: 'Untitled'}) - return ( - {cardTemplate.icon}
} - onClick={() => { - this.props.addCardFromTemplate(cardTemplate.id) - }} - rightIcon={ - - }/> - - { - this.props.editCardTemplate(cardTemplate.id) - }} - /> - } - id='delete' - name={intl.formatMessage({id: 'ViewHeader.delete-template', defaultMessage: 'Delete'})} - onClick={async () => { - await mutator.deleteBlock(cardTemplate, 'delete card template') - }} - /> - - - } - /> - ) - })} - - } - onClick={() => { - this.props.addCard() - }} - /> - - this.props.addCardTemplate()} - /> - - -
- ) - } - private sortDisplayOptions() { const {boardTree} = this.props diff --git a/webapp/src/components/viewMenu.tsx b/webapp/src/components/viewMenu.tsx index a3a9b508b..4906cde1e 100644 --- a/webapp/src/components/viewMenu.tsx +++ b/webapp/src/components/viewMenu.tsx @@ -20,6 +20,7 @@ type Props = { board: Board, showView: (id: string) => void intl: IntlShape + readonly: boolean } export class ViewMenu extends React.PureComponent { @@ -112,31 +113,34 @@ export class ViewMenu extends React.PureComponent { onClick={this.handleViewClick} />))} - {boardTree.views.length > 1 && + {!this.props.readonly && boardTree.views.length > 1 && } onClick={this.handleDeleteView} - />} - } - > - } - onClick={this.handleAddViewBoard} /> - } - onClick={this.handleAddViewTable} - /> - + } + {!this.props.readonly && + } + > + } + onClick={this.handleAddViewBoard} + /> + } + onClick={this.handleAddViewTable} + /> + + } ) } diff --git a/webapp/src/components/viewTitle.tsx b/webapp/src/components/viewTitle.tsx index 11305efb0..f18a5270b 100644 --- a/webapp/src/components/viewTitle.tsx +++ b/webapp/src/components/viewTitle.tsx @@ -19,6 +19,7 @@ import './viewTitle.scss' type Props = { board: Board intl: IntlShape + readonly: boolean } type State = { @@ -42,7 +43,7 @@ class ViewTitle extends React.Component { return ( <>
- {!board.icon && + {!this.props.readonly && !board.icon && } - {board.showDescription && + {!this.props.readonly && board.showDescription && } - {!board.showDescription && + {!this.props.readonly && !board.showDescription &&
@@ -106,6 +108,7 @@ class ViewTitle extends React.Component { onBlur={(text) => { mutator.changeDescription(board, text) }} + readonly={this.props.readonly} />
} diff --git a/webapp/src/components/workspaceComponent.tsx b/webapp/src/components/workspaceComponent.tsx index c33189dad..3e0b5566c 100644 --- a/webapp/src/components/workspaceComponent.tsx +++ b/webapp/src/components/workspaceComponent.tsx @@ -19,6 +19,7 @@ type Props = { showView: (id: string, boardId?: string) => void setSearchText: (text?: string) => void setLanguage: (lang: string) => void + readonly: boolean } class WorkspaceComponent extends React.PureComponent { @@ -28,13 +29,15 @@ class WorkspaceComponent extends React.PureComponent { Utils.assert(workspaceTree) const element = (
- + {!this.props.readonly && + + }
{(boardTree?.board.isTemplate) &&
@@ -66,6 +69,7 @@ class WorkspaceComponent extends React.PureComponent { boardTree={boardTree} setSearchText={setSearchText} showView={showView} + readonly={this.props.readonly} />) } @@ -75,6 +79,7 @@ class WorkspaceComponent extends React.PureComponent { boardTree={boardTree} setSearchText={setSearchText} showView={showView} + readonly={this.props.readonly} />) } diff --git a/webapp/src/pages/boardPage.tsx b/webapp/src/pages/boardPage.tsx index b14a6326d..31ac8a9f6 100644 --- a/webapp/src/pages/boardPage.tsx +++ b/webapp/src/pages/boardPage.tsx @@ -20,6 +20,7 @@ type State = { viewId: string workspaceTree: WorkspaceTree boardTree?: BoardTree + readonly: boolean } export default class BoardPage extends React.Component { @@ -30,11 +31,13 @@ export default class BoardPage extends React.Component { const queryString = new URLSearchParams(window.location.search) const boardId = queryString.get('id') || '' const viewId = queryString.get('v') || '' + const readonly = (queryString.get('r') === '1') this.state = { boardId, viewId, workspaceTree: new MutableWorkspaceTree(), + readonly, } Utils.log(`BoardPage. boardId: ${boardId}`) @@ -73,6 +76,10 @@ export default class BoardPage extends React.Component { return } + if (this.state.readonly) { + return + } + if (e.keyCode === 90 && !e.shiftKey && (e.ctrlKey || e.metaKey) && !e.altKey) { // Cmd+Z Utils.log('Undo') if (mutator.canUndo) { @@ -136,6 +143,7 @@ export default class BoardPage extends React.Component { this.setSearchText(text) }} setLanguage={this.props.setLanguage} + readonly={this.state.readonly} />
) @@ -246,6 +254,10 @@ export default class BoardPage extends React.Component { let newUrl = window.location.protocol + '//' + window.location.host + window.location.pathname if (boardId) { newUrl += `?id=${encodeURIComponent(boardId)}` + + if (this.state.readonly) { + newUrl += '&r=1' + } } window.history.pushState({path: newUrl}, '', newUrl) @@ -260,7 +272,10 @@ export default class BoardPage extends React.Component { this.attachToBoard(boardId, viewId) } - const newUrl = window.location.protocol + '//' + window.location.host + window.location.pathname + `?id=${encodeURIComponent(boardId)}&v=${encodeURIComponent(viewId)}` + let newUrl = window.location.protocol + '//' + window.location.host + window.location.pathname + `?id=${encodeURIComponent(boardId)}&v=${encodeURIComponent(viewId)}` + if (this.state.readonly) { + newUrl += '&r=1' + } window.history.pushState({path: newUrl}, '', newUrl) } diff --git a/webapp/src/widgets/editable.tsx b/webapp/src/widgets/editable.tsx index 43f30a20a..a15ec3bb8 100644 --- a/webapp/src/widgets/editable.tsx +++ b/webapp/src/widgets/editable.tsx @@ -10,6 +10,7 @@ type Props = { placeholderText?: string className?: string saveOnEsc?: boolean + readonly?: boolean onCancel?: () => void onSave?: (saveType: 'onEnter'|'onEsc'|'onBlur') => void @@ -65,6 +66,7 @@ export default class Editable extends React.Component { this.blur() } }} + readOnly={this.props.readonly} /> ) } diff --git a/webapp/src/widgets/menuWrapper.scss b/webapp/src/widgets/menuWrapper.scss index e2a8b904c..3c706691c 100644 --- a/webapp/src/widgets/menuWrapper.scss +++ b/webapp/src/widgets/menuWrapper.scss @@ -1,4 +1,8 @@ .MenuWrapper { position: relative; cursor: pointer; + + &.disabled { + cursor: default; + } } diff --git a/webapp/src/widgets/menuWrapper.tsx b/webapp/src/widgets/menuWrapper.tsx index 638aed948..f3f7c9b12 100644 --- a/webapp/src/widgets/menuWrapper.tsx +++ b/webapp/src/widgets/menuWrapper.tsx @@ -11,6 +11,7 @@ type Props = { isDisabled?: boolean; stopPropagationOnToggle?: boolean; className?: string + disabled?: boolean } type State = { @@ -68,6 +69,10 @@ export default class MenuWrapper extends React.PureComponent { } private toggle = (e: React.MouseEvent): void => { + if (this.props.disabled) { + return + } + /** * This is only here so that we can toggle the menus in the sidebar, because the default behavior of the mobile * version (ie the one that uses a modal) needs propagation to close the modal after selecting something @@ -84,15 +89,22 @@ export default class MenuWrapper extends React.PureComponent { public render(): JSX.Element { const {children} = this.props + let className = 'MenuWrapper' + if (this.props.disabled) { + className += ' disabled' + } + if (this.props.className) { + className += ' ' + this.props.className + } return (
{children ? Object.values(children)[0] : null} - {children && this.state.open ? Object.values(children)[1] : null} + {children && !this.props.disabled && this.state.open ? Object.values(children)[1] : null}
) }