mirror of
https://github.com/mattermost/focalboard.git
synced 2025-03-03 15:32:14 +02:00
Readonly support in UI
This commit is contained in:
parent
3e3f376582
commit
f22527e650
@ -1,24 +1,31 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-nested-callbacks */
|
||||
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
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().
|
||||
|
@ -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 {
|
||||
|
@ -18,6 +18,7 @@ type Props = {
|
||||
block: Board|Card
|
||||
size?: 's' | 'm' | 'l'
|
||||
intl: IntlShape
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
class BlockIconSelector extends React.Component<Props> {
|
||||
@ -41,10 +42,17 @@ class BlockIconSelector extends React.Component<Props> {
|
||||
if (!block.icon) {
|
||||
return null
|
||||
}
|
||||
let className = `octo-icon size-${size}`
|
||||
if (this.props.readonly) {
|
||||
className += ' readonly'
|
||||
}
|
||||
const iconElement = <div className={className}><span>{block.icon}</span></div>
|
||||
return (
|
||||
<div className='BlockIconSelector'>
|
||||
{this.props.readonly && iconElement}
|
||||
{!this.props.readonly &&
|
||||
<MenuWrapper>
|
||||
<div className={`octo-icon size-${size}`}><span>{block.icon}</span></div>
|
||||
{iconElement}
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
id='random'
|
||||
@ -67,6 +75,7 @@ class BlockIconSelector extends React.Component<Props> {
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ type BoardCardProps = {
|
||||
onDragEnd: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
onDrop: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
type BoardCardState = {
|
||||
@ -54,7 +55,7 @@ class BoardCard extends React.Component<BoardCardProps, BoardCardState> {
|
||||
const element = (
|
||||
<div
|
||||
className={className}
|
||||
draggable={true}
|
||||
draggable={!this.props.readonly}
|
||||
style={{opacity: this.state.isDragged ? 0.5 : 1}}
|
||||
onClick={this.props.onClick}
|
||||
onDragStart={(e) => {
|
||||
@ -86,28 +87,30 @@ class BoardCard extends React.Component<BoardCardProps, BoardCardState> {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuWrapper
|
||||
className='optionsMenu'
|
||||
stopPropagationOnToggle={true}
|
||||
>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu position='left'>
|
||||
<Menu.Text
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name={intl.formatMessage({id: 'BoardCard.delete', defaultMessage: 'Delete'})}
|
||||
onClick={() => mutator.deleteBlock(card, 'delete card')}
|
||||
/>
|
||||
<Menu.Text
|
||||
icon={<DuplicateIcon/>}
|
||||
id='duplicate'
|
||||
name={intl.formatMessage({id: 'BoardCard.duplicate', defaultMessage: 'Duplicate'})}
|
||||
onClick={() => {
|
||||
mutator.duplicateCard(card.id)
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
{!this.props.readonly &&
|
||||
<MenuWrapper
|
||||
className='optionsMenu'
|
||||
stopPropagationOnToggle={true}
|
||||
>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu position='left'>
|
||||
<Menu.Text
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name={intl.formatMessage({id: 'BoardCard.delete', defaultMessage: 'Delete'})}
|
||||
onClick={() => mutator.deleteBlock(card, 'delete card')}
|
||||
/>
|
||||
<Menu.Text
|
||||
icon={<DuplicateIcon/>}
|
||||
id='duplicate'
|
||||
name={intl.formatMessage({id: 'BoardCard.duplicate', defaultMessage: 'Duplicate'})}
|
||||
onClick={() => {
|
||||
mutator.duplicateCard(card.id)
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
}
|
||||
|
||||
<div className='octo-icontitle'>
|
||||
{ card.icon ? <div className='octo-icon'>{card.icon}</div> : undefined }
|
||||
|
@ -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<Props, State> {
|
||||
cardId={this.state.shownCardId}
|
||||
onClose={() => this.showCard(undefined)}
|
||||
showCard={(cardId) => this.showCard(cardId)}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
</RootPortal>}
|
||||
|
||||
@ -147,6 +149,7 @@ class BoardComponent extends React.Component<Props, State> {
|
||||
<ViewTitle
|
||||
key={board.id + board.title}
|
||||
board={board}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
|
||||
<div className='octo-board'>
|
||||
@ -159,6 +162,7 @@ class BoardComponent extends React.Component<Props, State> {
|
||||
addCardTemplate={this.addCardTemplate}
|
||||
editCardTemplate={this.editCardTemplate}
|
||||
withGroupBy={true}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
<div
|
||||
className='octo-board-header'
|
||||
@ -176,18 +180,21 @@ class BoardComponent extends React.Component<Props, State> {
|
||||
id='BoardComponent.hidden-columns'
|
||||
defaultMessage='Hidden columns'
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className='octo-board-header-cell narrow'>
|
||||
<Button
|
||||
onClick={this.addGroupClicked}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='BoardComponent.add-a-group'
|
||||
defaultMessage='+ Add a group'
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
{!this.props.readonly &&
|
||||
<div className='octo-board-header-cell narrow'>
|
||||
<Button
|
||||
onClick={this.addGroupClicked}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='BoardComponent.add-a-group'
|
||||
defaultMessage='+ Add a group'
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
@ -205,16 +212,18 @@ class BoardComponent extends React.Component<Props, State> {
|
||||
onDrop={() => this.onDropToColumn(group.option)}
|
||||
>
|
||||
{group.cards.map((card) => this.renderCard(card, visiblePropertyTemplates))}
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.addCard(group.option.id)
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='BoardComponent.neww'
|
||||
defaultMessage='+ New'
|
||||
/>
|
||||
</Button>
|
||||
{!this.props.readonly &&
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.addCard(group.option.id)
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='BoardComponent.neww'
|
||||
defaultMessage='+ New'
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
</BoardColumn>
|
||||
))}
|
||||
|
||||
@ -240,6 +249,7 @@ class BoardComponent extends React.Component<Props, State> {
|
||||
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<Props, State> {
|
||||
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<Props, State> {
|
||||
</div>
|
||||
<Button>{`${group.cards.length}`}</Button>
|
||||
<div className='octo-spacer'/>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
id='hide'
|
||||
icon={<HideIcon/>}
|
||||
name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
|
||||
onClick={() => mutator.hideViewColumn(activeView, '')}
|
||||
{!this.props.readonly &&
|
||||
<>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
id='hide'
|
||||
icon={<HideIcon/>}
|
||||
name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
|
||||
onClick={() => mutator.hideViewColumn(activeView, '')}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
<IconButton
|
||||
icon={<AddIcon/>}
|
||||
onClick={() => this.addCard(undefined)}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
<IconButton
|
||||
icon={<AddIcon/>}
|
||||
onClick={() => this.addCard(undefined)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -346,7 +360,7 @@ class BoardComponent extends React.Component<Props, State> {
|
||||
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<Props, State> {
|
||||
onChanged={(text) => {
|
||||
this.propertyNameChanged(group.option, text)
|
||||
}}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
<Button>{`${group.cards.length}`}</Button>
|
||||
<div className='octo-spacer'/>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
id='hide'
|
||||
icon={<HideIcon/>}
|
||||
name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
|
||||
onClick={() => mutator.hideViewColumn(activeView, group.option.id)}
|
||||
{!this.props.readonly &&
|
||||
<>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
id='hide'
|
||||
icon={<HideIcon/>}
|
||||
name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
|
||||
onClick={() => mutator.hideViewColumn(activeView, group.option.id)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='delete'
|
||||
icon={<DeleteIcon/>}
|
||||
name={intl.formatMessage({id: 'BoardComponent.delete', defaultMessage: 'Delete'})}
|
||||
onClick={() => mutator.deletePropertyOption(boardTree, boardTree.groupByProperty!, group.option)}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
{Constants.menuColors.map((color) => (
|
||||
<Menu.Color
|
||||
key={color.id}
|
||||
id={color.id}
|
||||
name={color.name}
|
||||
onClick={() => mutator.changePropertyOptionColor(boardTree.board, boardTree.groupByProperty!, group.option, color.id)}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
<IconButton
|
||||
icon={<AddIcon/>}
|
||||
onClick={() => this.addCard(group.option.id)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='delete'
|
||||
icon={<DeleteIcon/>}
|
||||
name={intl.formatMessage({id: 'BoardComponent.delete', defaultMessage: 'Delete'})}
|
||||
onClick={() => mutator.deletePropertyOption(boardTree, boardTree.groupByProperty!, group.option)}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
{Constants.menuColors.map((color) => (
|
||||
<Menu.Color
|
||||
key={color.id}
|
||||
id={color.id}
|
||||
name={color.name}
|
||||
onClick={() => mutator.changePropertyOptionColor(boardTree.board, boardTree.groupByProperty!, group.option, color.id)}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
<IconButton
|
||||
icon={<AddIcon/>}
|
||||
onClick={() => this.addCard(group.option.id)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -457,7 +476,9 @@ class BoardComponent extends React.Component<Props, State> {
|
||||
this.onDropToColumn(group.option)
|
||||
}}
|
||||
>
|
||||
<MenuWrapper>
|
||||
<MenuWrapper
|
||||
disabled={this.props.readonly}
|
||||
>
|
||||
<div
|
||||
key={group.option.id || 'empty'}
|
||||
className={`octo-label ${group.option.color}`}
|
||||
|
@ -30,6 +30,7 @@ type Props = {
|
||||
boardTree: BoardTree
|
||||
cardTree: CardTree
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
type State = {
|
||||
@ -74,6 +75,7 @@ class CardDetail extends React.Component<Props, State> {
|
||||
block={block}
|
||||
card={card}
|
||||
contents={cardTree.contents}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
))}
|
||||
</div>)
|
||||
@ -81,20 +83,22 @@ class CardDetail extends React.Component<Props, State> {
|
||||
contentElements = (<div className='octo-content'>
|
||||
<div className='octo-block'>
|
||||
<div className='octo-block-margin'/>
|
||||
<MarkdownEditor
|
||||
text=''
|
||||
placeholderText='Add a description...'
|
||||
onBlur={(text) => {
|
||||
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 &&
|
||||
<MarkdownEditor
|
||||
text=''
|
||||
placeholderText='Add a description...'
|
||||
onBlur={(text) => {
|
||||
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')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
@ -107,8 +111,9 @@ class CardDetail extends React.Component<Props, State> {
|
||||
<BlockIconSelector
|
||||
block={card}
|
||||
size='l'
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
{!icon &&
|
||||
{!this.props.readonly && !icon &&
|
||||
<div className='add-buttons'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@ -137,6 +142,7 @@ class CardDetail extends React.Component<Props, State> {
|
||||
}
|
||||
}}
|
||||
onCancel={() => this.setState({title: this.props.cardTree.card.title})}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
|
||||
{/* Property list */}
|
||||
@ -148,19 +154,22 @@ class CardDetail extends React.Component<Props, State> {
|
||||
key={propertyTemplate.id}
|
||||
className='octo-propertyrow'
|
||||
>
|
||||
<MenuWrapper>
|
||||
<div className='octo-propertyname'><Button>{propertyTemplate.name}</Button></div>
|
||||
<PropertyMenu
|
||||
propertyId={propertyTemplate.id}
|
||||
propertyName={propertyTemplate.name}
|
||||
propertyType={propertyTemplate.type}
|
||||
onNameChanged={(newName: string) => mutator.renameProperty(board, propertyTemplate.id, newName)}
|
||||
onTypeChanged={(newType: PropertyType) => mutator.changePropertyType(boardTree, propertyTemplate, newType)}
|
||||
onDelete={(id: string) => mutator.deleteProperty(boardTree, id)}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
{this.props.readonly && <div className='octo-propertyname'>{propertyTemplate.name}</div>}
|
||||
{!this.props.readonly &&
|
||||
<MenuWrapper>
|
||||
<div className='octo-propertyname'><Button>{propertyTemplate.name}</Button></div>
|
||||
<PropertyMenu
|
||||
propertyId={propertyTemplate.id}
|
||||
propertyName={propertyTemplate.name}
|
||||
propertyType={propertyTemplate.type}
|
||||
onNameChanged={(newName: string) => mutator.renameProperty(board, propertyTemplate.id, newName)}
|
||||
onTypeChanged={(newType: PropertyType) => mutator.changePropertyType(boardTree, propertyTemplate, newType)}
|
||||
onDelete={(id: string) => mutator.deleteProperty(boardTree, id)}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
}
|
||||
<PropertyValueElement
|
||||
readOnly={false}
|
||||
readOnly={this.props.readonly}
|
||||
card={card}
|
||||
boardTree={boardTree}
|
||||
propertyTemplate={propertyTemplate}
|
||||
@ -170,29 +179,35 @@ class CardDetail extends React.Component<Props, State> {
|
||||
)
|
||||
})}
|
||||
|
||||
<div className='octo-propertyname add-property'>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
// TODO: Show UI
|
||||
await mutator.insertPropertyTemplate(boardTree)
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='CardDetail.add-property'
|
||||
defaultMessage='+ Add a property'
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
{!this.props.readonly &&
|
||||
<div className='octo-propertyname add-property'>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
// TODO: Show UI
|
||||
await mutator.insertPropertyTemplate(boardTree)
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='CardDetail.add-property'
|
||||
defaultMessage='+ Add a property'
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Comments */}
|
||||
|
||||
<hr/>
|
||||
<CommentsList
|
||||
comments={comments}
|
||||
cardId={card.id}
|
||||
/>
|
||||
<hr/>
|
||||
{!this.props.readonly &&
|
||||
<>
|
||||
<hr/>
|
||||
<CommentsList
|
||||
comments={comments}
|
||||
cardId={card.id}
|
||||
/>
|
||||
<hr/>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Content blocks */}
|
||||
@ -201,38 +216,40 @@ class CardDetail extends React.Component<Props, State> {
|
||||
{contentElements}
|
||||
</div>
|
||||
|
||||
<div className='CardDetail content add-content'>
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
<FormattedMessage
|
||||
id='CardDetail.add-content'
|
||||
defaultMessage='Add content'
|
||||
/>
|
||||
</Button>
|
||||
<Menu position='top'>
|
||||
<Menu.Text
|
||||
id='text'
|
||||
name={intl.formatMessage({id: 'CardDetail.text', defaultMessage: 'Text'})}
|
||||
onClick={() => {
|
||||
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')
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='image'
|
||||
name={intl.formatMessage({id: 'CardDetail.image', defaultMessage: 'Image'})}
|
||||
onClick={() => Utils.selectLocalFile(
|
||||
(file) => mutator.createImageBlock(card, file, (this.props.cardTree.contents.length + 1) * 1000),
|
||||
'.jpg,.jpeg,.png',
|
||||
)}
|
||||
/>
|
||||
{!this.props.readonly &&
|
||||
<div className='CardDetail content add-content'>
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
<FormattedMessage
|
||||
id='CardDetail.add-content'
|
||||
defaultMessage='Add content'
|
||||
/>
|
||||
</Button>
|
||||
<Menu position='top'>
|
||||
<Menu.Text
|
||||
id='text'
|
||||
name={intl.formatMessage({id: 'CardDetail.text', defaultMessage: 'Text'})}
|
||||
onClick={() => {
|
||||
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')
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='image'
|
||||
name={intl.formatMessage({id: 'CardDetail.image', defaultMessage: 'Image'})}
|
||||
onClick={() => Utils.selectLocalFile(
|
||||
(file) => mutator.createImageBlock(card, file, (this.props.cardTree.contents.length + 1) * 1000),
|
||||
'.jpg,.jpeg,.png',
|
||||
)}
|
||||
/>
|
||||
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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<Props, State> {
|
||||
return (
|
||||
<Dialog
|
||||
onClose={this.props.onClose}
|
||||
toolsMenu={menu}
|
||||
toolsMenu={!this.props.readonly && menu}
|
||||
>
|
||||
{(cardTree?.card.isTemplate) &&
|
||||
<div className='banner'>
|
||||
@ -120,6 +121,7 @@ class CardDialog extends React.Component<Props, State> {
|
||||
<CardDetail
|
||||
boardTree={this.props.boardTree}
|
||||
cardTree={this.state.cardTree}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
}
|
||||
{(!this.state.cardTree && this.state.syncComplete) &&
|
||||
|
@ -29,6 +29,7 @@ type Props = {
|
||||
block: IOrderedBlock
|
||||
card: IBlock
|
||||
contents: readonly IOrderedBlock[]
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
class ContentBlock extends React.PureComponent<Props> {
|
||||
@ -44,89 +45,91 @@ class ContentBlock extends React.PureComponent<Props> {
|
||||
return (
|
||||
<div className='ContentBlock octo-block'>
|
||||
<div className='octo-block-margin'>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu>
|
||||
{index > 0 &&
|
||||
<Menu.Text
|
||||
id='moveUp'
|
||||
name='Move up'
|
||||
icon={<SortUpIcon/>}
|
||||
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) &&
|
||||
<Menu.Text
|
||||
id='moveDown'
|
||||
name='Move down'
|
||||
icon={<SortDownIcon/>}
|
||||
onClick={() => {
|
||||
const nextBlock = contents[index + 1]
|
||||
const newOrder = OctoUtils.getOrderAfter(nextBlock, contents)
|
||||
Utils.log(`moveDown ${newOrder}`)
|
||||
mutator.changeOrder(block, newOrder, 'move down')
|
||||
}}
|
||||
/>}
|
||||
<Menu.SubMenu
|
||||
id='insertAbove'
|
||||
name='Insert above'
|
||||
icon={<AddIcon/>}
|
||||
>
|
||||
<Menu.Text
|
||||
id='text'
|
||||
name='Text'
|
||||
icon={<TextIcon/>}
|
||||
onClick={() => {
|
||||
const newBlock = new MutableTextBlock()
|
||||
newBlock.parentId = card.id
|
||||
newBlock.rootId = card.rootId
|
||||
{!this.props.readonly &&
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu>
|
||||
{index > 0 &&
|
||||
<Menu.Text
|
||||
id='moveUp'
|
||||
name='Move up'
|
||||
icon={<SortUpIcon/>}
|
||||
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) &&
|
||||
<Menu.Text
|
||||
id='moveDown'
|
||||
name='Move down'
|
||||
icon={<SortDownIcon/>}
|
||||
onClick={() => {
|
||||
const nextBlock = contents[index + 1]
|
||||
const newOrder = OctoUtils.getOrderAfter(nextBlock, contents)
|
||||
Utils.log(`moveDown ${newOrder}`)
|
||||
mutator.changeOrder(block, newOrder, 'move down')
|
||||
}}
|
||||
/>}
|
||||
<Menu.SubMenu
|
||||
id='insertAbove'
|
||||
name='Insert above'
|
||||
icon={<AddIcon/>}
|
||||
>
|
||||
<Menu.Text
|
||||
id='text'
|
||||
name='Text'
|
||||
icon={<TextIcon/>}
|
||||
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')
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='image'
|
||||
name='Image'
|
||||
icon={<ImageIcon/>}
|
||||
onClick={() => {
|
||||
Utils.selectLocalFile(
|
||||
(file) => {
|
||||
mutator.createImageBlock(card, file, OctoUtils.getOrderBefore(block, contents))
|
||||
},
|
||||
'.jpg,.jpeg,.png')
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='divider'
|
||||
name='Divider'
|
||||
icon={<DividerIcon/>}
|
||||
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')
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='image'
|
||||
name='Image'
|
||||
icon={<ImageIcon/>}
|
||||
onClick={() => {
|
||||
Utils.selectLocalFile(
|
||||
(file) => {
|
||||
mutator.createImageBlock(card, file, OctoUtils.getOrderBefore(block, contents))
|
||||
},
|
||||
'.jpg,.jpeg,.png')
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='divider'
|
||||
name='Divider'
|
||||
icon={<DividerIcon/>}
|
||||
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')
|
||||
}}
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
<Menu.Text
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name='Delete'
|
||||
onClick={() => mutator.deleteBlock(block)}
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
<Menu.Text
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name='Delete'
|
||||
onClick={() => mutator.deleteBlock(block)}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
}
|
||||
</div>
|
||||
{block.type === 'text' &&
|
||||
<MarkdownEditor
|
||||
@ -136,6 +139,7 @@ class ContentBlock extends React.PureComponent<Props> {
|
||||
Utils.log(`change text ${block.id}, ${text}`)
|
||||
mutator.changeTitle(block, text, 'edit card text')
|
||||
}}
|
||||
readonly={this.props.readonly}
|
||||
/>}
|
||||
{block.type === 'divider' && <div className='divider'/>}
|
||||
{block.type === 'image' &&
|
||||
|
@ -47,20 +47,23 @@ export default class Dialog extends React.PureComponent<Props> {
|
||||
}}
|
||||
>
|
||||
<div className='dialog' >
|
||||
{toolsMenu &&
|
||||
<div className='toolbar'>
|
||||
<IconButton
|
||||
onClick={this.closeClicked}
|
||||
icon={<CloseIcon/>}
|
||||
title={'Close dialog'}
|
||||
className='hideOnWidescreen'
|
||||
/>
|
||||
<div className='octo-spacer'/>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
{toolsMenu}
|
||||
</MenuWrapper>
|
||||
</div>}
|
||||
{toolsMenu &&
|
||||
<>
|
||||
<IconButton
|
||||
onClick={this.closeClicked}
|
||||
icon={<CloseIcon/>}
|
||||
title={'Close dialog'}
|
||||
className='hideOnWidescreen'
|
||||
/>
|
||||
<div className='octo-spacer'/>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
{toolsMenu}
|
||||
</MenuWrapper>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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<Props> {
|
||||
<div
|
||||
ref={this.elementRef}
|
||||
className={className}
|
||||
contentEditable={true}
|
||||
contentEditable={!this.props.readonly}
|
||||
suppressContentEditableWarning={true}
|
||||
style={initialStyle}
|
||||
placeholder={placeholderText}
|
||||
@ -100,6 +101,10 @@ class Editable extends React.PureComponent<Props> {
|
||||
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<Props> {
|
||||
}}
|
||||
|
||||
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<Props> {
|
||||
}}
|
||||
|
||||
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()
|
||||
|
@ -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<Props, State> {
|
||||
}
|
||||
|
||||
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<Props, State> {
|
||||
style={{display: this.state.isEditing ? 'none' : undefined}}
|
||||
dangerouslySetInnerHTML={{__html: html}}
|
||||
onClick={() => {
|
||||
if (!this.state.isEditing) {
|
||||
if (!this.props.readonly && !this.state.isEditing) {
|
||||
this.showEditor()
|
||||
}
|
||||
}}
|
||||
|
113
webapp/src/components/newCardButton.tsx
Normal file
113
webapp/src/components/newCardButton.tsx
Normal file
@ -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<Props> {
|
||||
render(): JSX.Element {
|
||||
const {intl, boardTree} = this.props
|
||||
|
||||
return (
|
||||
<ButtonWithMenu
|
||||
onClick={() => {
|
||||
this.props.addCard()
|
||||
}}
|
||||
text={(
|
||||
<FormattedMessage
|
||||
id='ViewHeader.new'
|
||||
defaultMessage='New'
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Menu position='left'>
|
||||
{boardTree.cardTemplates.length > 0 && <>
|
||||
<Menu.Label>
|
||||
<b>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.select-a-template'
|
||||
defaultMessage='Select a template'
|
||||
/>
|
||||
</b>
|
||||
</Menu.Label>
|
||||
|
||||
<Menu.Separator/>
|
||||
</>}
|
||||
|
||||
{boardTree.cardTemplates.map((cardTemplate) => {
|
||||
const displayName = cardTemplate.title || intl.formatMessage({id: 'ViewHeader.untitled', defaultMessage: 'Untitled'})
|
||||
return (
|
||||
<Menu.Text
|
||||
key={cardTemplate.id}
|
||||
id={cardTemplate.id}
|
||||
name={displayName}
|
||||
icon={<div className='Icon'>{cardTemplate.icon}</div>}
|
||||
onClick={() => {
|
||||
this.props.addCardFromTemplate(cardTemplate.id)
|
||||
}}
|
||||
rightIcon={
|
||||
<MenuWrapper stopPropagationOnToggle={true}>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu position='left'>
|
||||
<Menu.Text
|
||||
id='edit'
|
||||
name={intl.formatMessage({id: 'ViewHeader.edit-template', defaultMessage: 'Edit'})}
|
||||
onClick={() => {
|
||||
this.props.editCardTemplate(cardTemplate.id)
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name={intl.formatMessage({id: 'ViewHeader.delete-template', defaultMessage: 'Delete'})}
|
||||
onClick={async () => {
|
||||
await mutator.deleteBlock(cardTemplate, 'delete card template')
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
<Menu.Text
|
||||
id='empty-template'
|
||||
name={intl.formatMessage({id: 'ViewHeader.empty-card', defaultMessage: 'Empty card'})}
|
||||
icon={<CardIcon/>}
|
||||
onClick={() => {
|
||||
this.props.addCard()
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu.Text
|
||||
id='add-template'
|
||||
name={intl.formatMessage({id: 'ViewHeader.add-template', defaultMessage: '+ New template'})}
|
||||
onClick={() => this.props.addCardTemplate()}
|
||||
/>
|
||||
</Menu>
|
||||
</ButtonWithMenu>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(NewCardButton)
|
@ -50,7 +50,7 @@ export default class PropertyValueElement extends React.Component<Props, State>
|
||||
}
|
||||
|
||||
if (propertyTemplate.type === 'select') {
|
||||
let className = 'octo-propertyvalue'
|
||||
let className = 'octo-propertyvalue octo-label'
|
||||
if (!displayValue) {
|
||||
className += ' empty'
|
||||
}
|
||||
|
@ -55,6 +55,8 @@
|
||||
|
||||
.octo-propertyvalue {
|
||||
line-height: 17px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.octo-editable,
|
||||
|
@ -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<Props, State> {
|
||||
cardId={this.state.shownCardId}
|
||||
onClose={() => this.showCard(undefined)}
|
||||
showCard={(cardId) => this.showCard(cardId)}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
</RootPortal>}
|
||||
<div className='octo-frame'>
|
||||
<ViewTitle
|
||||
key={board.id + board.title}
|
||||
board={board}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
|
||||
<div className='octo-table'>
|
||||
@ -97,6 +100,7 @@ class TableComponent extends React.Component<Props, State> {
|
||||
addCardFromTemplate={this.addCardFromTemplate}
|
||||
addCardTemplate={this.addCardTemplate}
|
||||
editCardTemplate={this.editCardTemplate}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
|
||||
{/* Main content */}
|
||||
@ -115,10 +119,11 @@ class TableComponent extends React.Component<Props, State> {
|
||||
className='octo-table-cell title-cell header-cell'
|
||||
style={{overflow: 'unset', width: this.columnWidth(Constants.titleColumnId)}}
|
||||
>
|
||||
<MenuWrapper>
|
||||
<MenuWrapper
|
||||
disabled={this.props.readonly}
|
||||
>
|
||||
<div
|
||||
className='octo-label'
|
||||
style={{cursor: 'pointer'}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='TableComponent.name'
|
||||
@ -134,32 +139,34 @@ class TableComponent extends React.Component<Props, State> {
|
||||
|
||||
<div className='octo-spacer'/>
|
||||
|
||||
<HorizontalGrip
|
||||
onDrag={(offset) => {
|
||||
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 &&
|
||||
<HorizontalGrip
|
||||
onDrag={(offset) => {
|
||||
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')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Table header row */}
|
||||
@ -199,11 +206,12 @@ class TableComponent extends React.Component<Props, State> {
|
||||
this.onDropToColumn(template)
|
||||
}}
|
||||
>
|
||||
<MenuWrapper>
|
||||
<MenuWrapper
|
||||
disabled={this.props.readonly}
|
||||
>
|
||||
<div
|
||||
className='octo-label'
|
||||
style={{cursor: 'pointer'}}
|
||||
draggable={true}
|
||||
draggable={!this.props.readonly}
|
||||
onDragStart={() => {
|
||||
this.draggedHeaderTemplate = template
|
||||
}}
|
||||
@ -222,32 +230,34 @@ class TableComponent extends React.Component<Props, State> {
|
||||
|
||||
<div className='octo-spacer'/>
|
||||
|
||||
<HorizontalGrip
|
||||
onDrag={(offset) => {
|
||||
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 &&
|
||||
<HorizontalGrip
|
||||
onDrag={(offset) => {
|
||||
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')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>)
|
||||
})}
|
||||
</div>
|
||||
@ -276,6 +286,7 @@ class TableComponent extends React.Component<Props, State> {
|
||||
}
|
||||
}}
|
||||
showCard={this.showCard}
|
||||
readonly={this.props.readonly}
|
||||
/>)
|
||||
|
||||
this.cardIdToRowMap.set(card.id, tableRowRef)
|
||||
@ -286,17 +297,19 @@ class TableComponent extends React.Component<Props, State> {
|
||||
{/* Add New row */}
|
||||
|
||||
<div className='octo-table-footer'>
|
||||
<div
|
||||
className='octo-table-cell'
|
||||
onClick={() => {
|
||||
this.addCard()
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='TableComponent.plus-new'
|
||||
defaultMessage='+ New'
|
||||
/>
|
||||
</div>
|
||||
{!this.props.readonly &&
|
||||
<div
|
||||
className='octo-table-cell'
|
||||
onClick={() => {
|
||||
this.addCard()
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='TableComponent.plus-new'
|
||||
defaultMessage='+ New'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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<Props, State> {
|
||||
}
|
||||
}}
|
||||
onCancel={() => this.setState({title: card.title})}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -99,7 +101,7 @@ class TableRow extends React.Component<Props, State> {
|
||||
style={{width: this.columnWidth(template.id)}}
|
||||
>
|
||||
<PropertyValueElement
|
||||
readOnly={false}
|
||||
readOnly={this.props.readonly}
|
||||
card={card}
|
||||
boardTree={boardTree}
|
||||
propertyTemplate={template}
|
||||
|
@ -15,11 +15,8 @@ import {CsvExporter} from '../csvExporter'
|
||||
import mutator from '../mutator'
|
||||
import {BoardTree} from '../viewModel/boardTree'
|
||||
import Button from '../widgets/buttons/button'
|
||||
import ButtonWithMenu from '../widgets/buttons/buttonWithMenu'
|
||||
import IconButton from '../widgets/buttons/iconButton'
|
||||
import CardIcon from '../widgets/icons/card'
|
||||
import CheckIcon from '../widgets/icons/check'
|
||||
import DeleteIcon from '../widgets/icons/delete'
|
||||
import DropdownIcon from '../widgets/icons/dropdown'
|
||||
import OptionsIcon from '../widgets/icons/options'
|
||||
import SortDownIcon from '../widgets/icons/sortDown'
|
||||
@ -29,6 +26,7 @@ import MenuWrapper from '../widgets/menuWrapper'
|
||||
|
||||
import {Editable} from './editable'
|
||||
import FilterComponent from './filterComponent'
|
||||
import NewCardButton from './newCardButton'
|
||||
import './viewHeader.scss'
|
||||
|
||||
type Props = {
|
||||
@ -41,6 +39,7 @@ type Props = {
|
||||
editCardTemplate: (cardTemplateId: string) => void
|
||||
withGroupBy?: boolean
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
type State = {
|
||||
@ -66,6 +65,282 @@ class ViewHeader extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className='ViewHeader'>
|
||||
<Editable
|
||||
style={{color: 'rgb(var(--main-fg))', fontWeight: 600}}
|
||||
text={activeView.title}
|
||||
placeholderText='Untitled View'
|
||||
onChanged={(text) => {
|
||||
mutator.changeTitle(activeView, text)
|
||||
}}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<DropdownIcon/>}/>
|
||||
<ViewMenu
|
||||
board={board}
|
||||
boardTree={boardTree}
|
||||
showView={showView}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
|
||||
<div className='octo-spacer'/>
|
||||
|
||||
{!this.props.readonly &&
|
||||
<>
|
||||
{/* Card properties */}
|
||||
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.properties'
|
||||
defaultMessage='Properties'
|
||||
/>
|
||||
</Button>
|
||||
<Menu>
|
||||
{boardTree.board.cardProperties.map((option: IPropertyTemplate) => (
|
||||
<Menu.Switch
|
||||
key={option.id}
|
||||
id={option.id}
|
||||
name={option.name}
|
||||
isOn={activeView.visiblePropertyIds.includes(option.id)}
|
||||
onClick={(propertyId: string) => {
|
||||
let newVisiblePropertyIds = []
|
||||
if (activeView.visiblePropertyIds.includes(propertyId)) {
|
||||
newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o: string) => o !== propertyId)
|
||||
} else {
|
||||
newVisiblePropertyIds = [...activeView.visiblePropertyIds, propertyId]
|
||||
}
|
||||
mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
|
||||
{/* Group by */}
|
||||
|
||||
{withGroupBy &&
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.group-by'
|
||||
defaultMessage='Group by {property}'
|
||||
values={{
|
||||
property: (
|
||||
<span
|
||||
style={{color: 'rgb(var(--main-fg))'}}
|
||||
id='groupByLabel'
|
||||
>
|
||||
{boardTree.groupByProperty?.name}
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
<Menu>
|
||||
{boardTree.board.cardProperties.filter((o: IPropertyTemplate) => o.type === 'select').map((option: IPropertyTemplate) => (
|
||||
<Menu.Text
|
||||
key={option.id}
|
||||
id={option.id}
|
||||
name={option.name}
|
||||
rightIcon={boardTree.activeView.groupById === option.id ? <CheckIcon/> : undefined}
|
||||
onClick={(id) => {
|
||||
if (boardTree.activeView.groupById === id) {
|
||||
return
|
||||
}
|
||||
|
||||
mutator.changeViewGroupById(boardTree.activeView, id)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</MenuWrapper>}
|
||||
|
||||
{/* Filter */}
|
||||
|
||||
<div className='filter-container'>
|
||||
<Button
|
||||
active={hasFilter}
|
||||
onClick={this.showFilterDialog}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.filter'
|
||||
defaultMessage='Filter'
|
||||
/>
|
||||
</Button>
|
||||
{this.state.showFilter &&
|
||||
<FilterComponent
|
||||
boardTree={boardTree}
|
||||
onClose={this.hideFilterDialog}
|
||||
/>}
|
||||
</div>
|
||||
|
||||
{/* Sort */}
|
||||
|
||||
<MenuWrapper>
|
||||
<Button active={hasSort}>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.sort'
|
||||
defaultMessage='Sort'
|
||||
/>
|
||||
</Button>
|
||||
<Menu>
|
||||
{(activeView.sortOptions.length > 0) &&
|
||||
<>
|
||||
<Menu.Text
|
||||
id='manual'
|
||||
name='Manual'
|
||||
onClick={() => {
|
||||
// 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')
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu.Text
|
||||
id='revert'
|
||||
name='Revert'
|
||||
onClick={() => {
|
||||
mutator.changeViewSortOptions(activeView, [])
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu.Separator/>
|
||||
</>
|
||||
}
|
||||
|
||||
{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 ? <SortUpIcon/> : <SortDownIcon/>
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Menu.Text
|
||||
key={option.id}
|
||||
id={option.id}
|
||||
name={option.name}
|
||||
rightIcon={rightIcon}
|
||||
onClick={(propertyId: string) => {
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</>
|
||||
}
|
||||
|
||||
{/* Search */}
|
||||
|
||||
{this.state.isSearching &&
|
||||
<Editable
|
||||
ref={this.searchFieldRef}
|
||||
text={boardTree.getSearchText()}
|
||||
placeholderText={intl.formatMessage({id: 'ViewHeader.search-text', defaultMessage: 'Search text'})}
|
||||
style={{color: 'rgb(var(--main-fg))'}}
|
||||
onChanged={(text) => {
|
||||
this.searchChanged(text)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
this.onSearchKeyDown(e)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
{!this.state.isSearching &&
|
||||
<Button onClick={() => this.setState({isSearching: true})}>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.search'
|
||||
defaultMessage='Search'
|
||||
/>
|
||||
</Button>}
|
||||
|
||||
{/* Options menu */}
|
||||
|
||||
{!this.props.readonly &&
|
||||
<>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
id='exportCsv'
|
||||
name={intl.formatMessage({id: 'ViewHeader.export-csv', defaultMessage: 'Export to CSV'})}
|
||||
onClick={() => CsvExporter.exportTableCsv(boardTree)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='exportBoardArchive'
|
||||
name={intl.formatMessage({id: 'ViewHeader.export-board-archive', defaultMessage: 'Export board archive'})}
|
||||
onClick={() => Archiver.exportBoardTree(boardTree)}
|
||||
/>
|
||||
|
||||
<Menu.Separator/>
|
||||
|
||||
<Menu.Text
|
||||
id='testAdd100Cards'
|
||||
name={intl.formatMessage({id: 'ViewHeader.test-add-100-cards', defaultMessage: 'TEST: Add 100 cards'})}
|
||||
onClick={() => this.testAddCards(100)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='testAdd1000Cards'
|
||||
name={intl.formatMessage({id: 'ViewHeader.test-add-1000-cards', defaultMessage: 'TEST: Add 1,000 cards'})}
|
||||
onClick={() => this.testAddCards(1000)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='testDistributeCards'
|
||||
name={intl.formatMessage({id: 'ViewHeader.test-distribute-cards', defaultMessage: 'TEST: Distribute cards'})}
|
||||
onClick={() => this.testDistributeCards()}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='testRandomizeIcons'
|
||||
name={intl.formatMessage({id: 'ViewHeader.test-randomize-icons', defaultMessage: 'TEST: Randomize icons'})}
|
||||
onClick={() => this.testRandomizeIcons()}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
|
||||
{/* New card button */}
|
||||
|
||||
<NewCardButton
|
||||
boardTree={this.props.boardTree}
|
||||
addCard={this.props.addCard}
|
||||
addCardFromTemplate={this.props.addCardFromTemplate}
|
||||
addCardTemplate={this.props.addCardTemplate}
|
||||
editCardTemplate={this.props.editCardTemplate}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private showFilterDialog = () => {
|
||||
this.setState({showFilter: true})
|
||||
}
|
||||
@ -145,320 +420,6 @@ class ViewHeader extends React.Component<Props, State> {
|
||||
})
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className='ViewHeader'>
|
||||
<Editable
|
||||
style={{color: 'rgb(var(--main-fg))', fontWeight: 600}}
|
||||
text={activeView.title}
|
||||
placeholderText='Untitled View'
|
||||
onChanged={(text) => {
|
||||
mutator.changeTitle(activeView, text)
|
||||
}}
|
||||
/>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<DropdownIcon/>}/>
|
||||
<ViewMenu
|
||||
board={board}
|
||||
boardTree={boardTree}
|
||||
showView={showView}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
<div className='octo-spacer'/>
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.properties'
|
||||
defaultMessage='Properties'
|
||||
/>
|
||||
</Button>
|
||||
<Menu>
|
||||
{boardTree.board.cardProperties.map((option: IPropertyTemplate) => (
|
||||
<Menu.Switch
|
||||
key={option.id}
|
||||
id={option.id}
|
||||
name={option.name}
|
||||
isOn={activeView.visiblePropertyIds.includes(option.id)}
|
||||
onClick={(propertyId: string) => {
|
||||
let newVisiblePropertyIds = []
|
||||
if (activeView.visiblePropertyIds.includes(propertyId)) {
|
||||
newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o: string) => o !== propertyId)
|
||||
} else {
|
||||
newVisiblePropertyIds = [...activeView.visiblePropertyIds, propertyId]
|
||||
}
|
||||
mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
{withGroupBy &&
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.group-by'
|
||||
defaultMessage='Group by {property}'
|
||||
values={{
|
||||
property: (
|
||||
<span
|
||||
style={{color: 'rgb(var(--main-fg))'}}
|
||||
id='groupByLabel'
|
||||
>
|
||||
{boardTree.groupByProperty?.name}
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
<Menu>
|
||||
{boardTree.board.cardProperties.filter((o: IPropertyTemplate) => o.type === 'select').map((option: IPropertyTemplate) => (
|
||||
<Menu.Text
|
||||
key={option.id}
|
||||
id={option.id}
|
||||
name={option.name}
|
||||
rightIcon={boardTree.activeView.groupById === option.id ? <CheckIcon/> : undefined}
|
||||
onClick={(id) => {
|
||||
if (boardTree.activeView.groupById === id) {
|
||||
return
|
||||
}
|
||||
|
||||
mutator.changeViewGroupById(boardTree.activeView, id)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</MenuWrapper>}
|
||||
<div className='filter-container'>
|
||||
<Button
|
||||
active={hasFilter}
|
||||
onClick={this.showFilterDialog}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.filter'
|
||||
defaultMessage='Filter'
|
||||
/>
|
||||
</Button>
|
||||
{this.state.showFilter &&
|
||||
<FilterComponent
|
||||
boardTree={boardTree}
|
||||
onClose={this.hideFilterDialog}
|
||||
/>}
|
||||
</div>
|
||||
<MenuWrapper>
|
||||
<Button active={hasSort}>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.sort'
|
||||
defaultMessage='Sort'
|
||||
/>
|
||||
</Button>
|
||||
<Menu>
|
||||
{(activeView.sortOptions.length > 0) &&
|
||||
<>
|
||||
<Menu.Text
|
||||
id='manual'
|
||||
name='Manual'
|
||||
onClick={() => {
|
||||
// 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')
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu.Text
|
||||
id='revert'
|
||||
name='Revert'
|
||||
onClick={() => {
|
||||
mutator.changeViewSortOptions(activeView, [])
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu.Separator/>
|
||||
</>
|
||||
}
|
||||
|
||||
{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 ? <SortUpIcon/> : <SortDownIcon/>
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Menu.Text
|
||||
key={option.id}
|
||||
id={option.id}
|
||||
name={option.name}
|
||||
rightIcon={rightIcon}
|
||||
onClick={(propertyId: string) => {
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
{this.state.isSearching &&
|
||||
<Editable
|
||||
ref={this.searchFieldRef}
|
||||
text={boardTree.getSearchText()}
|
||||
placeholderText={intl.formatMessage({id: 'ViewHeader.search-text', defaultMessage: 'Search text'})}
|
||||
style={{color: 'rgb(var(--main-fg))'}}
|
||||
onChanged={(text) => {
|
||||
this.searchChanged(text)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
this.onSearchKeyDown(e)
|
||||
}}
|
||||
/>}
|
||||
{!this.state.isSearching &&
|
||||
<Button onClick={() => this.setState({isSearching: true})}>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.search'
|
||||
defaultMessage='Search'
|
||||
/>
|
||||
</Button>}
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
id='exportCsv'
|
||||
name={intl.formatMessage({id: 'ViewHeader.export-csv', defaultMessage: 'Export to CSV'})}
|
||||
onClick={() => CsvExporter.exportTableCsv(boardTree)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='exportBoardArchive'
|
||||
name={intl.formatMessage({id: 'ViewHeader.export-board-archive', defaultMessage: 'Export board archive'})}
|
||||
onClick={() => Archiver.exportBoardTree(boardTree)}
|
||||
/>
|
||||
|
||||
<Menu.Separator/>
|
||||
|
||||
<Menu.Text
|
||||
id='testAdd100Cards'
|
||||
name={intl.formatMessage({id: 'ViewHeader.test-add-100-cards', defaultMessage: 'TEST: Add 100 cards'})}
|
||||
onClick={() => this.testAddCards(100)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='testAdd1000Cards'
|
||||
name={intl.formatMessage({id: 'ViewHeader.test-add-1000-cards', defaultMessage: 'TEST: Add 1,000 cards'})}
|
||||
onClick={() => this.testAddCards(1000)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='testDistributeCards'
|
||||
name={intl.formatMessage({id: 'ViewHeader.test-distribute-cards', defaultMessage: 'TEST: Distribute cards'})}
|
||||
onClick={() => this.testDistributeCards()}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='testRandomizeIcons'
|
||||
name={intl.formatMessage({id: 'ViewHeader.test-randomize-icons', defaultMessage: 'TEST: Randomize icons'})}
|
||||
onClick={() => this.testRandomizeIcons()}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
|
||||
<ButtonWithMenu
|
||||
onClick={() => {
|
||||
this.props.addCard()
|
||||
}}
|
||||
text={(
|
||||
<FormattedMessage
|
||||
id='ViewHeader.new'
|
||||
defaultMessage='New'
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Menu position='left'>
|
||||
{boardTree.cardTemplates.length > 0 && <>
|
||||
<Menu.Label>
|
||||
<b>
|
||||
<FormattedMessage
|
||||
id='ViewHeader.select-a-template'
|
||||
defaultMessage='Select a template'
|
||||
/>
|
||||
</b>
|
||||
</Menu.Label>
|
||||
|
||||
<Menu.Separator/>
|
||||
</>}
|
||||
|
||||
{boardTree.cardTemplates.map((cardTemplate) => {
|
||||
const displayName = cardTemplate.title || intl.formatMessage({id: 'ViewHeader.untitled', defaultMessage: 'Untitled'})
|
||||
return (
|
||||
<Menu.Text
|
||||
key={cardTemplate.id}
|
||||
id={cardTemplate.id}
|
||||
name={displayName}
|
||||
icon={<div className='Icon'>{cardTemplate.icon}</div>}
|
||||
onClick={() => {
|
||||
this.props.addCardFromTemplate(cardTemplate.id)
|
||||
}}
|
||||
rightIcon={
|
||||
<MenuWrapper stopPropagationOnToggle={true}>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu position='left'>
|
||||
<Menu.Text
|
||||
id='edit'
|
||||
name={intl.formatMessage({id: 'ViewHeader.edit-template', defaultMessage: 'Edit'})}
|
||||
onClick={() => {
|
||||
this.props.editCardTemplate(cardTemplate.id)
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name={intl.formatMessage({id: 'ViewHeader.delete-template', defaultMessage: 'Delete'})}
|
||||
onClick={async () => {
|
||||
await mutator.deleteBlock(cardTemplate, 'delete card template')
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
<Menu.Text
|
||||
id='empty-template'
|
||||
name={intl.formatMessage({id: 'ViewHeader.empty-card', defaultMessage: 'Empty card'})}
|
||||
icon={<CardIcon/>}
|
||||
onClick={() => {
|
||||
this.props.addCard()
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu.Text
|
||||
id='add-template'
|
||||
name={intl.formatMessage({id: 'ViewHeader.add-template', defaultMessage: '+ New template'})}
|
||||
onClick={() => this.props.addCardTemplate()}
|
||||
/>
|
||||
</Menu>
|
||||
</ButtonWithMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private sortDisplayOptions() {
|
||||
const {boardTree} = this.props
|
||||
|
||||
|
@ -20,6 +20,7 @@ type Props = {
|
||||
board: Board,
|
||||
showView: (id: string) => void
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
export class ViewMenu extends React.PureComponent<Props> {
|
||||
@ -112,31 +113,34 @@ export class ViewMenu extends React.PureComponent<Props> {
|
||||
onClick={this.handleViewClick}
|
||||
/>))}
|
||||
<Menu.Separator/>
|
||||
{boardTree.views.length > 1 &&
|
||||
{!this.props.readonly && boardTree.views.length > 1 &&
|
||||
<Menu.Text
|
||||
id='__deleteView'
|
||||
name='Delete View'
|
||||
icon={<DeleteIcon/>}
|
||||
onClick={this.handleDeleteView}
|
||||
/>}
|
||||
<Menu.SubMenu
|
||||
id='__addView'
|
||||
name='Add View'
|
||||
icon={<AddIcon/>}
|
||||
>
|
||||
<Menu.Text
|
||||
id='board'
|
||||
name='Board'
|
||||
icon={<BoardIcon/>}
|
||||
onClick={this.handleAddViewBoard}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='table'
|
||||
name='Table'
|
||||
icon={<TableIcon/>}
|
||||
onClick={this.handleAddViewTable}
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
}
|
||||
{!this.props.readonly &&
|
||||
<Menu.SubMenu
|
||||
id='__addView'
|
||||
name='Add View'
|
||||
icon={<AddIcon/>}
|
||||
>
|
||||
<Menu.Text
|
||||
id='board'
|
||||
name='Board'
|
||||
icon={<BoardIcon/>}
|
||||
onClick={this.handleAddViewBoard}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='table'
|
||||
name='Table'
|
||||
icon={<TableIcon/>}
|
||||
onClick={this.handleAddViewTable}
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
@ -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<Props, State> {
|
||||
return (
|
||||
<>
|
||||
<div className='ViewTitle add-buttons add-visible'>
|
||||
{!board.icon &&
|
||||
{!this.props.readonly && !board.icon &&
|
||||
<Button
|
||||
onClick={() => {
|
||||
const newIcon = BlockIcons.shared.randomIcon()
|
||||
@ -56,7 +57,7 @@ class ViewTitle extends React.Component<Props, State> {
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
{board.showDescription &&
|
||||
{!this.props.readonly && board.showDescription &&
|
||||
<Button
|
||||
onClick={() => {
|
||||
mutator.showDescription(board, false)
|
||||
@ -69,7 +70,7 @@ class ViewTitle extends React.Component<Props, State> {
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
{!board.showDescription &&
|
||||
{!this.props.readonly && !board.showDescription &&
|
||||
<Button
|
||||
onClick={() => {
|
||||
mutator.showDescription(board, true)
|
||||
@ -95,6 +96,7 @@ class ViewTitle extends React.Component<Props, State> {
|
||||
saveOnEsc={true}
|
||||
onSave={() => mutator.changeTitle(board, this.state.title)}
|
||||
onCancel={() => this.setState({title: this.props.board.title})}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -106,6 +108,7 @@ class ViewTitle extends React.Component<Props, State> {
|
||||
onBlur={(text) => {
|
||||
mutator.changeDescription(board, text)
|
||||
}}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
@ -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<Props> {
|
||||
@ -28,13 +29,15 @@ class WorkspaceComponent extends React.PureComponent<Props> {
|
||||
Utils.assert(workspaceTree)
|
||||
const element = (
|
||||
<div className='WorkspaceComponent'>
|
||||
<Sidebar
|
||||
showBoard={showBoard}
|
||||
showView={showView}
|
||||
workspaceTree={workspaceTree}
|
||||
activeBoardId={boardTree?.board.id}
|
||||
setLanguage={setLanguage}
|
||||
/>
|
||||
{!this.props.readonly &&
|
||||
<Sidebar
|
||||
showBoard={showBoard}
|
||||
showView={showView}
|
||||
workspaceTree={workspaceTree}
|
||||
activeBoardId={boardTree?.board.id}
|
||||
setLanguage={setLanguage}
|
||||
/>
|
||||
}
|
||||
<div className='mainFrame'>
|
||||
{(boardTree?.board.isTemplate) &&
|
||||
<div className='banner'>
|
||||
@ -66,6 +69,7 @@ class WorkspaceComponent extends React.PureComponent<Props> {
|
||||
boardTree={boardTree}
|
||||
setSearchText={setSearchText}
|
||||
showView={showView}
|
||||
readonly={this.props.readonly}
|
||||
/>)
|
||||
}
|
||||
|
||||
@ -75,6 +79,7 @@ class WorkspaceComponent extends React.PureComponent<Props> {
|
||||
boardTree={boardTree}
|
||||
setSearchText={setSearchText}
|
||||
showView={showView}
|
||||
readonly={this.props.readonly}
|
||||
/>)
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ type State = {
|
||||
viewId: string
|
||||
workspaceTree: WorkspaceTree
|
||||
boardTree?: BoardTree
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
export default class BoardPage extends React.Component<Props, State> {
|
||||
@ -30,11 +31,13 @@ export default class BoardPage extends React.Component<Props, State> {
|
||||
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<Props, State> {
|
||||
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<Props, State> {
|
||||
this.setSearchText(text)
|
||||
}}
|
||||
setLanguage={this.props.setLanguage}
|
||||
readonly={this.state.readonly}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@ -246,6 +254,10 @@ export default class BoardPage extends React.Component<Props, State> {
|
||||
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<Props, State> {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -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<Props> {
|
||||
this.blur()
|
||||
}
|
||||
}}
|
||||
readOnly={this.props.readonly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
.MenuWrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
@ -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<Props, State> {
|
||||
}
|
||||
|
||||
private toggle = (e: React.MouseEvent<HTMLDivElement, 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<Props, State> {
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={`MenuWrapper ${this.props.className || ''}`}
|
||||
className={className}
|
||||
onClick={this.toggle}
|
||||
ref={this.node}
|
||||
>
|
||||
{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}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user