mirror of
https://github.com/mattermost/focalboard.git
synced 2024-12-03 08:45:40 +02:00
Merge branch 'main' into auth
This commit is contained in:
commit
dc5fb0cfc1
@ -2,6 +2,7 @@ package sqlstore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
@ -46,7 +47,7 @@ func (s *SQLStore) Migrate() error {
|
||||
}
|
||||
|
||||
err = m.Up()
|
||||
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
if err != nil && !errors.Is(err, migrate.ErrNoChange) && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0'>
|
||||
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
|
||||
<link rel="stylesheet" href="/static/easymde.min.css">
|
||||
|
@ -25,20 +25,32 @@
|
||||
"Filter.not-includes": "doesn't include",
|
||||
"FilterComponent.add-filter": "+ Add Filter",
|
||||
"FilterComponent.delete": "Delete",
|
||||
"Mutator.duplicate-board": "duplicate board",
|
||||
"Mutator.new-board-from-template": "new board from template",
|
||||
"Mutator.new-card-from-template": "new card from template",
|
||||
"Mutator.new-template-from-board": "new template from board",
|
||||
"Mutator.new-template-from-card": "new template from card",
|
||||
"Sidebar.add-board": "+ Add Board",
|
||||
"Sidebar.add-template": "+ New template",
|
||||
"Sidebar.dark-theme": "Dark Theme",
|
||||
"Sidebar.delete-board": "Delete Board",
|
||||
"Sidebar.delete-template": "Delete",
|
||||
"Sidebar.duplicate-board": "Duplicate Board",
|
||||
"Sidebar.edit-template": "Edit",
|
||||
"Sidebar.empty-board": "Empty board",
|
||||
"Sidebar.english": "English",
|
||||
"Sidebar.export-archive": "Export Archive",
|
||||
"Sidebar.import-archive": "Import Archive",
|
||||
"Sidebar.light-theme": "Light Theme",
|
||||
"Sidebar.mattermost-theme": "Mattermost Theme",
|
||||
"Sidebar.no-views-in-board": "No pages inside",
|
||||
"Sidebar.select-a-template": "Select a template",
|
||||
"Sidebar.set-language": "Set Language",
|
||||
"Sidebar.set-theme": "Set Theme",
|
||||
"Sidebar.settings": "Settings",
|
||||
"Sidebar.spanish": "Spanish",
|
||||
"Sidebar.template-from-board": "New template from board",
|
||||
"Sidebar.untitled": "Untitled",
|
||||
"Sidebar.untitled-board": "(Untitled Board)",
|
||||
"Sidebar.untitled-view": "(Untitled View)",
|
||||
"TableComponent.add-icon": "Add Icon",
|
||||
@ -73,8 +85,11 @@
|
||||
"ViewHeader.test-distribute-cards": "TEST: Distribute cards",
|
||||
"ViewHeader.test-randomize-icons": "TEST: Randomize icons",
|
||||
"ViewHeader.untitled": "Untitled",
|
||||
"ViewTitle.hide-description": "hide description",
|
||||
"ViewTitle.pick-icon": "Pick Icon",
|
||||
"ViewTitle.random-icon": "Random",
|
||||
"ViewTitle.remove-icon": "Remove Icon",
|
||||
"ViewTitle.untitled-board": "Untitled Board"
|
||||
"ViewTitle.show-description": "show description",
|
||||
"ViewTitle.untitled-board": "Untitled Board",
|
||||
"WorkspaceComponent.editing-board-template": "You're editing a board template"
|
||||
}
|
@ -26,7 +26,9 @@
|
||||
"jest": {
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
}
|
||||
},
|
||||
"collectCoverage": true,
|
||||
"collectCoverageFrom": ["src/**/*.{ts,tsx,js,jsx}"]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^2.13.2",
|
||||
|
@ -1,5 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {Utils} from '../utils'
|
||||
|
||||
import {IBlock} from '../blocks/block'
|
||||
|
||||
import {MutableBlock} from './block'
|
||||
@ -35,7 +37,11 @@ interface IMutablePropertyTemplate extends IPropertyTemplate {
|
||||
|
||||
interface Board extends IBlock {
|
||||
readonly icon: string
|
||||
readonly description: string
|
||||
readonly showDescription: boolean
|
||||
readonly isTemplate: boolean
|
||||
readonly cardProperties: readonly IPropertyTemplate[]
|
||||
duplicate(): MutableBoard
|
||||
}
|
||||
|
||||
class MutableBoard extends MutableBlock {
|
||||
@ -46,6 +52,27 @@ class MutableBoard extends MutableBlock {
|
||||
this.fields.icon = value
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.fields.description as string
|
||||
}
|
||||
set description(value: string) {
|
||||
this.fields.description = value
|
||||
}
|
||||
|
||||
get showDescription(): boolean {
|
||||
return Boolean(this.fields.showDescription)
|
||||
}
|
||||
set showDescription(value: boolean) {
|
||||
this.fields.showDescription = value
|
||||
}
|
||||
|
||||
get isTemplate(): boolean {
|
||||
return Boolean(this.fields.isTemplate)
|
||||
}
|
||||
set isTemplate(value: boolean) {
|
||||
this.fields.isTemplate = value
|
||||
}
|
||||
|
||||
get cardProperties(): IMutablePropertyTemplate[] {
|
||||
return this.fields.cardProperties as IPropertyTemplate[]
|
||||
}
|
||||
@ -58,6 +85,7 @@ class MutableBoard extends MutableBlock {
|
||||
this.type = 'board'
|
||||
|
||||
this.icon = block.fields?.icon || ''
|
||||
this.description = block.fields?.description || ''
|
||||
if (block.fields?.cardProperties) {
|
||||
// Deep clone of card properties and their options
|
||||
this.cardProperties = block.fields.cardProperties.map((o: IPropertyTemplate) => {
|
||||
@ -72,6 +100,12 @@ class MutableBoard extends MutableBlock {
|
||||
this.cardProperties = []
|
||||
}
|
||||
}
|
||||
|
||||
duplicate(): MutableBoard {
|
||||
const card = new MutableBoard(this)
|
||||
card.id = Utils.createGuid()
|
||||
return card
|
||||
}
|
||||
}
|
||||
|
||||
export {Board, MutableBoard, PropertyType, IPropertyOption, IPropertyTemplate}
|
||||
|
@ -21,7 +21,7 @@ class MutableCard extends MutableBlock {
|
||||
}
|
||||
|
||||
get isTemplate(): boolean {
|
||||
return this.fields.isTemplate as boolean
|
||||
return Boolean(this.fields.isTemplate)
|
||||
}
|
||||
set isTemplate(value: boolean) {
|
||||
this.fields.isTemplate = value
|
||||
|
@ -5,7 +5,6 @@ import React from 'react'
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
|
||||
|
||||
import {BlockIcons} from '../blockIcons'
|
||||
import {IBlock} from '../blocks/block'
|
||||
import {IPropertyOption, IPropertyTemplate} from '../blocks/board'
|
||||
import {Card, MutableCard} from '../blocks/card'
|
||||
import {CardFilter} from '../cardFilter'
|
||||
@ -13,7 +12,6 @@ import {Constants} from '../constants'
|
||||
import mutator from '../mutator'
|
||||
import {Utils} from '../utils'
|
||||
import {BoardTree, BoardTreeGroup} from '../viewModel/boardTree'
|
||||
import {MutableCardTree} from '../viewModel/cardTree'
|
||||
import Button from '../widgets/buttons/button'
|
||||
import IconButton from '../widgets/buttons/iconButton'
|
||||
import AddIcon from '../widgets/icons/add'
|
||||
@ -27,7 +25,7 @@ import MenuWrapper from '../widgets/menuWrapper'
|
||||
import BoardCard from './boardCard'
|
||||
import {BoardColumn} from './boardColumn'
|
||||
import './boardComponent.scss'
|
||||
import {CardDialog} from './cardDialog'
|
||||
import CardDialog from './cardDialog'
|
||||
import {Editable} from './editable'
|
||||
import RootPortal from './rootPortal'
|
||||
import ViewHeader from './viewHeader'
|
||||
@ -279,19 +277,19 @@ class BoardComponent extends React.Component<Props, State> {
|
||||
}}
|
||||
|
||||
onDragOver={(e) => {
|
||||
ref.current!.classList.add('dragover')
|
||||
ref.current?.classList.add('dragover')
|
||||
e.preventDefault()
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
ref.current!.classList.add('dragover')
|
||||
ref.current?.classList.add('dragover')
|
||||
e.preventDefault()
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
ref.current!.classList.remove('dragover')
|
||||
ref.current?.classList.remove('dragover')
|
||||
e.preventDefault()
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
ref.current!.classList.remove('dragover')
|
||||
ref.current?.classList.remove('dragover')
|
||||
e.preventDefault()
|
||||
this.onDropToColumn(group.option)
|
||||
}}
|
||||
@ -348,19 +346,19 @@ class BoardComponent extends React.Component<Props, State> {
|
||||
}}
|
||||
|
||||
onDragOver={(e) => {
|
||||
ref.current!.classList.add('dragover')
|
||||
ref.current?.classList.add('dragover')
|
||||
e.preventDefault()
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
ref.current!.classList.add('dragover')
|
||||
ref.current?.classList.add('dragover')
|
||||
e.preventDefault()
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
ref.current!.classList.remove('dragover')
|
||||
ref.current?.classList.remove('dragover')
|
||||
e.preventDefault()
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
ref.current!.classList.remove('dragover')
|
||||
ref.current?.classList.remove('dragover')
|
||||
e.preventDefault()
|
||||
this.onDropToColumn(group.option)
|
||||
}}
|
||||
@ -424,25 +422,25 @@ class BoardComponent extends React.Component<Props, State> {
|
||||
if (this.draggedCards?.length < 1) {
|
||||
return
|
||||
}
|
||||
ref.current!.classList.add('dragover')
|
||||
ref.current?.classList.add('dragover')
|
||||
e.preventDefault()
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
if (this.draggedCards?.length < 1) {
|
||||
return
|
||||
}
|
||||
ref.current!.classList.add('dragover')
|
||||
ref.current?.classList.add('dragover')
|
||||
e.preventDefault()
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
if (this.draggedCards?.length < 1) {
|
||||
return
|
||||
}
|
||||
ref.current!.classList.remove('dragover')
|
||||
ref.current?.classList.remove('dragover')
|
||||
e.preventDefault()
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
ref.current!.classList.remove('dragover')
|
||||
ref.current?.classList.remove('dragover')
|
||||
e.preventDefault()
|
||||
if (this.draggedCards?.length < 1) {
|
||||
return
|
||||
@ -478,28 +476,25 @@ class BoardComponent extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
private addCardFromTemplate = async (cardTemplateId?: string) => {
|
||||
this.addCard(undefined, cardTemplateId)
|
||||
private addCardFromTemplate = async (cardTemplateId: string) => {
|
||||
await mutator.duplicateCard(
|
||||
cardTemplateId,
|
||||
this.props.intl.formatMessage({id: 'Mutator.new-card-from-template', defaultMessage: 'new card from template'}),
|
||||
false,
|
||||
async (newCardId) => {
|
||||
this.setState({shownCardId: newCardId})
|
||||
},
|
||||
async () => {
|
||||
this.setState({shownCardId: undefined})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private async addCard(groupByOptionId?: string, cardTemplateId?: string): Promise<void> {
|
||||
private async addCard(groupByOptionId?: string): Promise<void> {
|
||||
const {boardTree} = this.props
|
||||
const {activeView, board} = boardTree
|
||||
|
||||
let card: MutableCard
|
||||
let blocksToInsert: IBlock[]
|
||||
if (cardTemplateId) {
|
||||
const templateCardTree = new MutableCardTree(cardTemplateId)
|
||||
await templateCardTree.sync()
|
||||
const newCardTree = templateCardTree.templateCopy()
|
||||
card = newCardTree.card
|
||||
card.isTemplate = false
|
||||
card.title = ''
|
||||
blocksToInsert = [newCardTree.card, ...newCardTree.contents]
|
||||
} else {
|
||||
card = new MutableCard()
|
||||
blocksToInsert = [card]
|
||||
}
|
||||
const card = new MutableCard()
|
||||
|
||||
card.parentId = boardTree.board.id
|
||||
const propertiesThatMeetFilters = CardFilter.propertiesThatMeetFilterGroup(activeView.filter, board.cardProperties)
|
||||
@ -511,9 +506,11 @@ class BoardComponent extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
card.properties = {...card.properties, ...propertiesThatMeetFilters}
|
||||
if (!card.icon) {
|
||||
card.icon = BlockIcons.shared.randomIcon()
|
||||
await mutator.insertBlocks(
|
||||
blocksToInsert,
|
||||
}
|
||||
await mutator.insertBlock(
|
||||
card,
|
||||
'add card',
|
||||
async () => {
|
||||
this.setState({shownCardId: card.id})
|
||||
|
@ -44,8 +44,10 @@ class CardDetail extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
if (!this.state.title) {
|
||||
this.titleRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
|
||||
|
||||
import mutator from '../mutator'
|
||||
import {OctoListener} from '../octoListener'
|
||||
@ -19,6 +19,7 @@ type Props = {
|
||||
cardId: string
|
||||
onClose: () => void
|
||||
showCard: (cardId?: string) => void
|
||||
intl: IntlShape
|
||||
}
|
||||
|
||||
type State = {
|
||||
@ -94,7 +95,7 @@ class CardDialog extends React.Component<Props, State> {
|
||||
<Menu.Text
|
||||
id='makeTemplate'
|
||||
name='New template from card'
|
||||
onClick={this.makeTemplate}
|
||||
onClick={this.makeTemplateClicked}
|
||||
/>
|
||||
}
|
||||
</Menu>
|
||||
@ -122,25 +123,19 @@ class CardDialog extends React.Component<Props, State> {
|
||||
)
|
||||
}
|
||||
|
||||
private makeTemplate = async () => {
|
||||
private makeTemplateClicked = async () => {
|
||||
const {cardTree} = this.state
|
||||
if (!cardTree) {
|
||||
Utils.assertFailure('this.state.cardTree')
|
||||
return
|
||||
}
|
||||
|
||||
const newCardTree = cardTree.templateCopy()
|
||||
newCardTree.card.isTemplate = true
|
||||
newCardTree.card.title = 'New Template'
|
||||
|
||||
Utils.log(`Created new template: ${newCardTree.card.id}`)
|
||||
|
||||
const blocksToInsert = [newCardTree.card, ...newCardTree.contents]
|
||||
await mutator.insertBlocks(
|
||||
blocksToInsert,
|
||||
'create template from card',
|
||||
async () => {
|
||||
this.props.showCard(newCardTree.card.id)
|
||||
await mutator.duplicateCard(
|
||||
cardTree.card.id,
|
||||
this.props.intl.formatMessage({id: 'Mutator.new-template-from-card', defaultMessage: 'new template from card'}),
|
||||
true,
|
||||
async (newCardId) => {
|
||||
this.props.showCard(newCardId)
|
||||
},
|
||||
async () => {
|
||||
this.props.showCard(undefined)
|
||||
@ -149,4 +144,4 @@ class CardDialog extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export {CardDialog}
|
||||
export default injectIntl(CardDialog)
|
||||
|
@ -17,13 +17,27 @@
|
||||
background-color: rgb(var(--main-bg));
|
||||
border-radius: 3px;
|
||||
box-shadow: rgba(var(--main-fg), 0.1) 0px 0px 0px 1px, rgba(var(--main-fg), 0.1) 0px 2px 4px;
|
||||
margin: 72px auto;
|
||||
padding: 0;
|
||||
max-width: 975px;
|
||||
height: calc(100% - 144px);
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
@media not screen and (max-width: 975px) {
|
||||
margin: 72px auto;
|
||||
max-width: 975px;
|
||||
height: calc(100% - 144px);
|
||||
}
|
||||
@media screen and (max-width: 975px) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
> * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
> .banner {
|
||||
background-color: rgba(230, 220, 192, 0.9);
|
||||
text-align: center;
|
||||
@ -33,13 +47,26 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 30px;
|
||||
margin: 10px
|
||||
margin: 10px;
|
||||
|
||||
> .IconButton:first-child {
|
||||
/* Hide close button on larger screens */
|
||||
@media not screen and (max-width: 975px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 10px 126px 10px 126px;
|
||||
|
||||
@media not screen and (max-width: 975px) {
|
||||
padding: 10px 126px;
|
||||
}
|
||||
@media screen and (max-width: 975px) {
|
||||
padding: 10px 10px;
|
||||
}
|
||||
}
|
||||
> .content.fullwidth {
|
||||
padding: 10px 0 10px 0;
|
||||
|
@ -2,10 +2,10 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import MenuWrapper from '../widgets/menuWrapper'
|
||||
import OptionsIcon from '../widgets/icons/options'
|
||||
import IconButton from '../widgets/buttons/iconButton'
|
||||
|
||||
import CloseIcon from '../widgets/icons/close'
|
||||
import OptionsIcon from '../widgets/icons/options'
|
||||
import MenuWrapper from '../widgets/menuWrapper'
|
||||
import './dialog.scss'
|
||||
|
||||
type Props = {
|
||||
@ -23,17 +23,13 @@ export default class Dialog extends React.PureComponent<Props> {
|
||||
document.removeEventListener('keydown', this.keydownHandler)
|
||||
}
|
||||
|
||||
private close(): void {
|
||||
this.props.onClose()
|
||||
}
|
||||
|
||||
private keydownHandler = (e: KeyboardEvent): void => {
|
||||
if (e.target !== document.body) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.keyCode === 27) {
|
||||
this.close()
|
||||
this.closeClicked()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
@ -46,13 +42,18 @@ export default class Dialog extends React.PureComponent<Props> {
|
||||
className='Dialog dialog-back'
|
||||
onMouseDown={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
this.close()
|
||||
this.closeClicked()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='dialog' >
|
||||
{toolsMenu &&
|
||||
<div className='toolbar'>
|
||||
<IconButton
|
||||
onClick={this.closeClicked}
|
||||
icon={<CloseIcon/>}
|
||||
title={'Close dialog'}
|
||||
/>
|
||||
<div className='octo-spacer'/>
|
||||
<MenuWrapper>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
@ -64,4 +65,8 @@ export default class Dialog extends React.PureComponent<Props> {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private closeClicked = () => {
|
||||
this.props.onClose()
|
||||
}
|
||||
}
|
||||
|
@ -32,12 +32,17 @@ class Editable extends React.PureComponent<Props> {
|
||||
return this.privateText
|
||||
}
|
||||
set text(value: string) {
|
||||
if (!this.elementRef.current) {
|
||||
Utils.assertFailure('elementRef.current')
|
||||
return
|
||||
}
|
||||
|
||||
const {isMarkdown} = this.props
|
||||
|
||||
if (value) {
|
||||
this.elementRef.current!.innerHTML = isMarkdown ? Utils.htmlFromMarkdown(value) : Utils.htmlEncode(value)
|
||||
this.elementRef.current.innerHTML = isMarkdown ? Utils.htmlFromMarkdown(value) : Utils.htmlEncode(value)
|
||||
} else {
|
||||
this.elementRef.current!.innerText = ''
|
||||
this.elementRef.current.innerText = ''
|
||||
}
|
||||
|
||||
this.privateText = value || ''
|
||||
@ -55,7 +60,7 @@ class Editable extends React.PureComponent<Props> {
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.elementRef.current!.focus()
|
||||
this.elementRef.current?.focus()
|
||||
|
||||
// Put cursor at end
|
||||
document.execCommand('selectAll', false, undefined)
|
||||
@ -63,7 +68,7 @@ class Editable extends React.PureComponent<Props> {
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
this.elementRef.current!.blur()
|
||||
this.elementRef.current?.blur()
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
@ -90,9 +95,11 @@ class Editable extends React.PureComponent<Props> {
|
||||
dangerouslySetInnerHTML={{__html: html}}
|
||||
|
||||
onFocus={() => {
|
||||
this.elementRef.current!.innerText = this.text
|
||||
this.elementRef.current!.style!.color = style?.color || ''
|
||||
this.elementRef.current!.classList.add('active')
|
||||
if (this.elementRef.current) {
|
||||
this.elementRef.current.innerText = this.text
|
||||
this.elementRef.current.style.color = style?.color || ''
|
||||
this.elementRef.current.classList.add('active')
|
||||
}
|
||||
|
||||
if (onFocus) {
|
||||
onFocus()
|
||||
@ -100,7 +107,8 @@ class Editable extends React.PureComponent<Props> {
|
||||
}}
|
||||
|
||||
onBlur={async () => {
|
||||
const newText = this.elementRef.current!.innerText
|
||||
if (this.elementRef.current) {
|
||||
const newText = this.elementRef.current.innerText
|
||||
const oldText = this.props.text || ''
|
||||
if (this.props.allowEmpty || newText) {
|
||||
if (newText !== oldText && onChanged) {
|
||||
@ -112,7 +120,9 @@ class Editable extends React.PureComponent<Props> {
|
||||
this.text = oldText // Reset text
|
||||
}
|
||||
|
||||
this.elementRef.current!.classList.remove('active')
|
||||
this.elementRef.current.classList.remove('active')
|
||||
}
|
||||
|
||||
if (onBlur) {
|
||||
onBlur()
|
||||
}
|
||||
@ -121,10 +131,10 @@ class Editable extends React.PureComponent<Props> {
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === 27 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // ESC
|
||||
e.stopPropagation()
|
||||
this.elementRef.current!.blur()
|
||||
this.elementRef.current?.blur()
|
||||
} else if (!isMultiline && e.keyCode === 13 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // Return
|
||||
e.stopPropagation()
|
||||
this.elementRef.current!.blur()
|
||||
this.elementRef.current?.blur()
|
||||
}
|
||||
|
||||
if (onKeyDown) {
|
||||
|
@ -152,9 +152,8 @@ class MarkdownEditor extends React.Component<Props, State> {
|
||||
this.hideEditor()
|
||||
},
|
||||
focus: () => {
|
||||
this.frameRef.current!.classList.add('active')
|
||||
|
||||
this.elementRef.current!.setState({value: this.text})
|
||||
this.frameRef.current?.classList.add('active')
|
||||
this.elementRef.current?.setState({value: this.text})
|
||||
|
||||
if (onFocus) {
|
||||
onFocus()
|
||||
|
@ -8,13 +8,12 @@
|
||||
color: rgb(var(--sidebar-fg));
|
||||
background-color: rgb(var(--sidebar-bg));
|
||||
padding: 10px 0;
|
||||
overflow-y: scroll;
|
||||
|
||||
&.hidden {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 15;
|
||||
z-index: 5;
|
||||
min-height: 0;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
@ -30,6 +29,12 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.octo-sidebar-list {
|
||||
overflow-y: auto;
|
||||
max-height: calc(100% - 78px);
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.octo-sidebar-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -37,7 +42,7 @@
|
||||
font-weight: 600;
|
||||
padding: 3px 20px;
|
||||
margin-bottom: 5px;
|
||||
.IconButton {
|
||||
>.IconButton {
|
||||
background-color: var(--sidebar-bg);
|
||||
&:hover {
|
||||
background-color: rgba(var(--sidebar-fg), 0.1);
|
||||
@ -82,7 +87,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.IconButton {
|
||||
>.IconButton {
|
||||
background-color: var(--sidebar-bg);
|
||||
&:hover {
|
||||
background-color: rgba(var(--sidebar-fg), 0.1);
|
||||
@ -122,6 +127,10 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.Menu .OptionsIcon {
|
||||
fill: unset;
|
||||
}
|
||||
|
||||
.HideSidebarIcon {
|
||||
stroke: rgba(var(--sidebar-fg), 0.5);
|
||||
stroke-width: 6px;
|
||||
|
@ -12,13 +12,13 @@ import {WorkspaceTree} from '../viewModel/workspaceTree'
|
||||
import Button from '../widgets/buttons/button'
|
||||
import IconButton from '../widgets/buttons/iconButton'
|
||||
import DeleteIcon from '../widgets/icons/delete'
|
||||
import DisclosureTriangle from '../widgets/icons/disclosureTriangle'
|
||||
import DotIcon from '../widgets/icons/dot'
|
||||
import DuplicateIcon from '../widgets/icons/duplicate'
|
||||
import HamburgerIcon from '../widgets/icons/hamburger'
|
||||
import HideSidebarIcon from '../widgets/icons/hideSidebar'
|
||||
import OptionsIcon from '../widgets/icons/options'
|
||||
import ShowSidebarIcon from '../widgets/icons/showSidebar'
|
||||
import DisclosureTriangle from '../widgets/icons/disclosureTriangle'
|
||||
import Menu from '../widgets/menu'
|
||||
import MenuWrapper from '../widgets/menuWrapper'
|
||||
import './sidebar.scss'
|
||||
@ -87,6 +87,7 @@ class Sidebar extends React.Component<Props, State> {
|
||||
icon={<HideSidebarIcon/>}
|
||||
/>
|
||||
</div>
|
||||
<div className='octo-sidebar-list'>
|
||||
{
|
||||
boards.map((board) => {
|
||||
const displayTitle: string = board.title || intl.formatMessage({id: 'Sidebar.untitled-board', defaultMessage: '(Untitled Board)'})
|
||||
@ -137,17 +138,16 @@ class Sidebar extends React.Component<Props, State> {
|
||||
id='duplicateBoard'
|
||||
name={intl.formatMessage({id: 'Sidebar.duplicate-board', defaultMessage: 'Duplicate Board'})}
|
||||
icon={<DuplicateIcon/>}
|
||||
onClick={async () => {
|
||||
await mutator.duplicateBoard(
|
||||
board.id,
|
||||
'duplicate board',
|
||||
async (newBoardId) => {
|
||||
newBoardId && this.props.showBoard(newBoardId)
|
||||
},
|
||||
async () => {
|
||||
this.props.showBoard(board.id)
|
||||
},
|
||||
)
|
||||
onClick={() => {
|
||||
this.duplicateBoard(board.id)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu.Text
|
||||
id='templateFromBoard'
|
||||
name={intl.formatMessage({id: 'Sidebar.template-from-board', defaultMessage: 'New template from board'})}
|
||||
onClick={() => {
|
||||
this.addTemplateFromBoard(board.id)
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
@ -184,14 +184,78 @@ class Sidebar extends React.Component<Props, State> {
|
||||
|
||||
<br/>
|
||||
|
||||
<Button
|
||||
onClick={this.addBoardClicked}
|
||||
>
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
<FormattedMessage
|
||||
id='Sidebar.add-board'
|
||||
defaultMessage='+ Add Board'
|
||||
/>
|
||||
</Button>
|
||||
<Menu position='top'>
|
||||
<Menu.Label>
|
||||
<b>
|
||||
<FormattedMessage
|
||||
id='Sidebar.select-a-template'
|
||||
defaultMessage='Select a template'
|
||||
/>
|
||||
</b>
|
||||
</Menu.Label>
|
||||
|
||||
<Menu.Separator/>
|
||||
|
||||
{workspaceTree.boardTemplates.map((boardTemplate) => {
|
||||
let displayName = boardTemplate.title || intl.formatMessage({id: 'Sidebar.untitled', defaultMessage: 'Untitled'})
|
||||
if (boardTemplate.icon) {
|
||||
displayName = `${boardTemplate.icon} ${displayName}`
|
||||
}
|
||||
return (
|
||||
<Menu.Text
|
||||
key={boardTemplate.id}
|
||||
id={boardTemplate.id}
|
||||
name={displayName}
|
||||
onClick={() => {
|
||||
this.addBoardFromTemplate(boardTemplate.id)
|
||||
}}
|
||||
rightIcon={
|
||||
<MenuWrapper stopPropagationOnToggle={true}>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu position='left'>
|
||||
<Menu.Text
|
||||
id='edit'
|
||||
name={intl.formatMessage({id: 'Sidebar.edit-template', defaultMessage: 'Edit'})}
|
||||
onClick={() => {
|
||||
this.props.showBoard(boardTemplate.id)
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name={intl.formatMessage({id: 'Sidebar.delete-template', defaultMessage: 'Delete'})}
|
||||
onClick={async () => {
|
||||
await mutator.deleteBlock(boardTemplate, 'delete board template')
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
<Menu.Text
|
||||
id='empty-template'
|
||||
name={intl.formatMessage({id: 'Sidebar.empty-board', defaultMessage: 'Empty board'})}
|
||||
onClick={this.addBoardClicked}
|
||||
/>
|
||||
|
||||
<Menu.Text
|
||||
id='add-template'
|
||||
name={intl.formatMessage({id: 'Sidebar.add-template', defaultMessage: '+ New template'})}
|
||||
onClick={this.addBoardTemplateClicked}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
|
||||
<div className='octo-spacer'/>
|
||||
|
||||
@ -268,7 +332,9 @@ class Sidebar extends React.Component<Props, State> {
|
||||
const {showBoard, intl} = this.props
|
||||
|
||||
const oldBoardId = this.props.activeBoardId
|
||||
|
||||
const board = new MutableBoard()
|
||||
|
||||
const view = new MutableBoardView()
|
||||
view.viewType = 'board'
|
||||
view.parentId = board.id
|
||||
@ -288,6 +354,78 @@ class Sidebar extends React.Component<Props, State> {
|
||||
)
|
||||
}
|
||||
|
||||
private async addBoardFromTemplate(boardTemplateId: string) {
|
||||
const oldBoardId = this.props.activeBoardId
|
||||
|
||||
await mutator.duplicateBoard(
|
||||
boardTemplateId,
|
||||
this.props.intl.formatMessage({id: 'Mutator.new-board-from-template', defaultMessage: 'new board from template'}),
|
||||
false,
|
||||
async (newBoardId) => {
|
||||
this.props.showBoard(newBoardId)
|
||||
},
|
||||
async () => {
|
||||
if (oldBoardId) {
|
||||
this.props.showBoard(oldBoardId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private async duplicateBoard(boardId: string) {
|
||||
const oldBoardId = this.props.activeBoardId
|
||||
|
||||
await mutator.duplicateBoard(
|
||||
boardId,
|
||||
this.props.intl.formatMessage({id: 'Mutator.duplicate-board', defaultMessage: 'duplicate board'}),
|
||||
false,
|
||||
async (newBoardId) => {
|
||||
this.props.showBoard(newBoardId)
|
||||
},
|
||||
async () => {
|
||||
if (oldBoardId) {
|
||||
this.props.showBoard(oldBoardId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private async addTemplateFromBoard(boardId: string) {
|
||||
const oldBoardId = this.props.activeBoardId
|
||||
|
||||
await mutator.duplicateBoard(
|
||||
boardId,
|
||||
this.props.intl.formatMessage({id: 'Mutator.new-template-from-board', defaultMessage: 'new template from board'}),
|
||||
true,
|
||||
async (newBoardId) => {
|
||||
this.props.showBoard(newBoardId)
|
||||
},
|
||||
async () => {
|
||||
if (oldBoardId) {
|
||||
this.props.showBoard(oldBoardId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private addBoardTemplateClicked = async () => {
|
||||
const {activeBoardId} = this.props
|
||||
|
||||
const boardTemplate = new MutableBoard()
|
||||
boardTemplate.isTemplate = true
|
||||
await mutator.insertBlock(
|
||||
boardTemplate,
|
||||
'add board template',
|
||||
async () => {
|
||||
this.props.showBoard(boardTemplate.id)
|
||||
}, async () => {
|
||||
if (activeBoardId) {
|
||||
this.props.showBoard(activeBoardId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private hideClicked = () => {
|
||||
this.setState({isHidden: true})
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
|
||||
|
||||
import {BlockIcons} from '../blockIcons'
|
||||
import {IBlock} from '../blocks/block'
|
||||
import {IPropertyTemplate} from '../blocks/board'
|
||||
import {MutableBoardView} from '../blocks/boardView'
|
||||
import {MutableCard} from '../blocks/card'
|
||||
@ -12,12 +11,11 @@ import {Constants} from '../constants'
|
||||
import mutator from '../mutator'
|
||||
import {Utils} from '../utils'
|
||||
import {BoardTree} from '../viewModel/boardTree'
|
||||
import {MutableCardTree} from '../viewModel/cardTree'
|
||||
import SortDownIcon from '../widgets/icons/sortDown'
|
||||
import SortUpIcon from '../widgets/icons/sortUp'
|
||||
import MenuWrapper from '../widgets/menuWrapper'
|
||||
|
||||
import {CardDialog} from './cardDialog'
|
||||
import CardDialog from './cardDialog'
|
||||
import {HorizontalGrip} from './horizontalGrip'
|
||||
import RootPortal from './rootPortal'
|
||||
import './tableComponent.scss'
|
||||
@ -30,6 +28,7 @@ type Props = {
|
||||
boardTree: BoardTree
|
||||
showView: (id: string) => void
|
||||
setSearchText: (text?: string) => void
|
||||
intl: IntlShape
|
||||
}
|
||||
|
||||
type State = {
|
||||
@ -127,13 +126,17 @@ class TableComponent extends React.Component<Props, State> {
|
||||
onDrag={(offset) => {
|
||||
const originalWidth = this.columnWidth(Constants.titleColumnId)
|
||||
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||
titleRef.current!.style!.width = `${newWidth}px`
|
||||
if (titleRef.current) {
|
||||
titleRef.current.style.width = `${newWidth}px`
|
||||
}
|
||||
}}
|
||||
onDragEnd={(offset) => {
|
||||
Utils.log(`onDragEnd offset: ${offset}`)
|
||||
const originalWidth = this.columnWidth(Constants.titleColumnId)
|
||||
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||
titleRef.current!.style!.width = `${newWidth}px`
|
||||
if (titleRef.current) {
|
||||
titleRef.current.style.width = `${newWidth}px`
|
||||
}
|
||||
|
||||
const columnWidths = {...activeView.columnWidths}
|
||||
if (newWidth !== columnWidths[Constants.titleColumnId]) {
|
||||
@ -211,13 +214,17 @@ class TableComponent extends React.Component<Props, State> {
|
||||
onDrag={(offset) => {
|
||||
const originalWidth = this.columnWidth(template.id)
|
||||
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||
headerRef.current!.style.width = `${newWidth}px`
|
||||
if (headerRef.current) {
|
||||
headerRef.current.style.width = `${newWidth}px`
|
||||
}
|
||||
}}
|
||||
onDragEnd={(offset) => {
|
||||
Utils.log(`onDragEnd offset: ${offset}`)
|
||||
const originalWidth = this.columnWidth(template.id)
|
||||
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||
headerRef.current!.style.width = `${newWidth}px`
|
||||
if (headerRef.current) {
|
||||
headerRef.current.style.width = `${newWidth}px`
|
||||
}
|
||||
|
||||
const columnWidths = {...activeView.columnWidths}
|
||||
if (newWidth !== columnWidths[template.id]) {
|
||||
@ -246,7 +253,7 @@ class TableComponent extends React.Component<Props, State> {
|
||||
|
||||
const tableRow = (
|
||||
<TableRow
|
||||
key={card.id}
|
||||
key={card.id + card.updateAt}
|
||||
ref={tableRowRef}
|
||||
boardTree={boardTree}
|
||||
card={card}
|
||||
@ -296,32 +303,31 @@ class TableComponent extends React.Component<Props, State> {
|
||||
this.addCard(true)
|
||||
}
|
||||
|
||||
private addCardFromTemplate = async (cardTemplateId?: string) => {
|
||||
this.addCard(true, cardTemplateId)
|
||||
private addCardFromTemplate = async (cardTemplateId: string) => {
|
||||
await mutator.duplicateCard(
|
||||
cardTemplateId,
|
||||
this.props.intl.formatMessage({id: 'Mutator.new-card-from-template', defaultMessage: 'new card from template'}),
|
||||
false,
|
||||
async (newCardId) => {
|
||||
this.setState({shownCardId: newCardId})
|
||||
},
|
||||
async () => {
|
||||
this.setState({shownCardId: undefined})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private addCard = async (show = false, cardTemplateId?: string) => {
|
||||
private addCard = async (show = false) => {
|
||||
const {boardTree} = this.props
|
||||
|
||||
let card: MutableCard
|
||||
let blocksToInsert: IBlock[]
|
||||
if (cardTemplateId) {
|
||||
const templateCardTree = new MutableCardTree(cardTemplateId)
|
||||
await templateCardTree.sync()
|
||||
const newCardTree = templateCardTree.templateCopy()
|
||||
card = newCardTree.card
|
||||
card.isTemplate = false
|
||||
card.title = ''
|
||||
blocksToInsert = [newCardTree.card, ...newCardTree.contents]
|
||||
} else {
|
||||
card = new MutableCard()
|
||||
blocksToInsert = [card]
|
||||
}
|
||||
const card = new MutableCard()
|
||||
|
||||
card.parentId = boardTree.board.id
|
||||
if (!card.icon) {
|
||||
card.icon = BlockIcons.shared.randomIcon()
|
||||
await mutator.insertBlocks(
|
||||
blocksToInsert,
|
||||
}
|
||||
await mutator.insertBlock(
|
||||
card,
|
||||
'add card',
|
||||
async () => {
|
||||
if (show) {
|
||||
@ -375,4 +381,4 @@ class TableComponent extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export {TableComponent}
|
||||
export default injectIntl(TableComponent)
|
||||
|
@ -40,7 +40,7 @@ class TableRow extends React.Component<Props, State> {
|
||||
|
||||
componentDidMount(): void {
|
||||
if (this.props.focusOnMount) {
|
||||
setTimeout(() => this.titleRef.current!.focus(), 10)
|
||||
setTimeout(() => this.titleRef.current?.focus(), 10)
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,6 @@ class TableRow extends React.Component<Props, State> {
|
||||
return (
|
||||
<div
|
||||
className='TableRow octo-table-row'
|
||||
key={card.id}
|
||||
>
|
||||
|
||||
{/* Name / title */}
|
||||
|
@ -35,7 +35,7 @@ type Props = {
|
||||
showView: (id: string) => void
|
||||
setSearchText: (text?: string) => void
|
||||
addCard: () => void
|
||||
addCardFromTemplate: (cardTemplateId?: string) => void
|
||||
addCardFromTemplate: (cardTemplateId: string) => void
|
||||
addCardTemplate: () => void
|
||||
editCardTemplate: (cardTemplateId: string) => void
|
||||
withGroupBy?: boolean
|
||||
@ -61,7 +61,7 @@ class ViewHeader extends React.Component<Props, State> {
|
||||
|
||||
componentDidUpdate(prevPros: Props, prevState: State): void {
|
||||
if (this.state.isSearching && !prevState.isSearching) {
|
||||
this.searchFieldRef.current!.focus()
|
||||
this.searchFieldRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +75,9 @@ class ViewHeader extends React.Component<Props, State> {
|
||||
|
||||
private onSearchKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.keyCode === 27) { // ESC: Clear search
|
||||
this.searchFieldRef.current!.text = ''
|
||||
if (this.searchFieldRef.current) {
|
||||
this.searchFieldRef.current.text = ''
|
||||
}
|
||||
this.setState({isSearching: false})
|
||||
this.props.setSearchText(undefined)
|
||||
e.preventDefault()
|
||||
@ -397,11 +399,15 @@ class ViewHeader extends React.Component<Props, State> {
|
||||
<Menu.Separator/>
|
||||
|
||||
{boardTree.cardTemplates.map((cardTemplate) => {
|
||||
let displayName = cardTemplate.title || intl.formatMessage({id: 'ViewHeader.untitled', defaultMessage: 'Untitled'})
|
||||
if (cardTemplate.icon) {
|
||||
displayName = `${cardTemplate.icon} ${displayName}`
|
||||
}
|
||||
return (
|
||||
<Menu.Text
|
||||
key={cardTemplate.id}
|
||||
id={cardTemplate.id}
|
||||
name={cardTemplate.title || intl.formatMessage({id: 'ViewHeader.untitled', defaultMessage: 'Untitled'})}
|
||||
name={displayName}
|
||||
onClick={() => {
|
||||
this.props.addCardFromTemplate(cardTemplate.id)
|
||||
}}
|
||||
|
@ -5,7 +5,7 @@
|
||||
align-items: center;
|
||||
|
||||
&.add-buttons {
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
min-height: 30px;
|
||||
color:rgba(var(--main-fg), 0.4);
|
||||
width: 100%;
|
||||
@ -28,4 +28,8 @@
|
||||
margin-bottom: 0px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.description > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,11 @@ import mutator from '../mutator'
|
||||
import Button from '../widgets/buttons/button'
|
||||
import Editable from '../widgets/editable'
|
||||
import EmojiIcon from '../widgets/icons/emoji'
|
||||
import HideIcon from '../widgets/icons/hide'
|
||||
import ShowIcon from '../widgets/icons/show'
|
||||
|
||||
import BlockIconSelector from './blockIconSelector'
|
||||
import {MarkdownEditor} from './markdownEditor'
|
||||
import './viewTitle.scss'
|
||||
|
||||
type Props = {
|
||||
@ -38,7 +41,8 @@ class ViewTitle extends React.Component<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'ViewTitle add-buttons ' + (board.icon ? '' : 'add-visible')}>
|
||||
<div className='ViewTitle add-buttons add-visible'>
|
||||
{!board.icon &&
|
||||
<Button
|
||||
onClick={() => {
|
||||
const newIcon = BlockIcons.shared.randomIcon()
|
||||
@ -51,6 +55,33 @@ class ViewTitle extends React.Component<Props, State> {
|
||||
defaultMessage='Add Icon'
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
{board.showDescription &&
|
||||
<Button
|
||||
onClick={() => {
|
||||
mutator.showDescription(board, false)
|
||||
}}
|
||||
icon={<HideIcon/>}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='ViewTitle.hide-description'
|
||||
defaultMessage='hide description'
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
{!board.showDescription &&
|
||||
<Button
|
||||
onClick={() => {
|
||||
mutator.showDescription(board, true)
|
||||
}}
|
||||
icon={<ShowIcon/>}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='ViewTitle.show-description'
|
||||
defaultMessage='show description'
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='ViewTitle'>
|
||||
@ -66,6 +97,18 @@ class ViewTitle extends React.Component<Props, State> {
|
||||
onCancel={() => this.setState({title: this.props.board.title})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{board.showDescription &&
|
||||
<div className='ViewTitle description'>
|
||||
<MarkdownEditor
|
||||
text={board.description}
|
||||
placeholderText='Add a description...'
|
||||
onBlur={(text) => {
|
||||
mutator.changeDescription(board, text)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -3,4 +3,17 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: auto;
|
||||
|
||||
> .mainFrame {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
|
||||
> .banner {
|
||||
background-color: rgba(230, 220, 192, 0.9);
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {Utils} from '../utils'
|
||||
import {BoardTree} from '../viewModel/boardTree'
|
||||
@ -8,7 +9,7 @@ import {WorkspaceTree} from '../viewModel/workspaceTree'
|
||||
|
||||
import BoardComponent from './boardComponent'
|
||||
import Sidebar from './sidebar'
|
||||
import {TableComponent} from './tableComponent'
|
||||
import TableComponent from './tableComponent'
|
||||
import './workspaceComponent.scss'
|
||||
|
||||
type Props = {
|
||||
@ -34,7 +35,17 @@ class WorkspaceComponent extends React.PureComponent<Props> {
|
||||
activeBoardId={boardTree?.board.id}
|
||||
setLanguage={setLanguage}
|
||||
/>
|
||||
<div className='mainFrame'>
|
||||
{(boardTree?.board.isTemplate) &&
|
||||
<div className='banner'>
|
||||
<FormattedMessage
|
||||
id='WorkspaceComponent.editing-board-template'
|
||||
defaultMessage="You're editing a board template"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{this.mainComponent()}
|
||||
</div>
|
||||
</div>)
|
||||
|
||||
return element
|
||||
|
@ -156,6 +156,22 @@ class Mutator {
|
||||
await this.updateBlock(newBlock, block, description)
|
||||
}
|
||||
|
||||
async changeDescription(block: IBlock, boardDescription: string, description = 'change description') {
|
||||
const newBoard = new MutableBoard(block)
|
||||
newBoard.description = boardDescription
|
||||
await this.updateBlock(newBoard, block, description)
|
||||
}
|
||||
|
||||
async showDescription(board: Board, showDescription = true, description?: string) {
|
||||
const newBoard = new MutableBoard(board)
|
||||
newBoard.showDescription = showDescription
|
||||
let actionDescription = description
|
||||
if (!actionDescription) {
|
||||
actionDescription = showDescription ? 'show description' : 'hide description'
|
||||
}
|
||||
await this.updateBlock(newBoard, board, actionDescription)
|
||||
}
|
||||
|
||||
async changeOrder(block: IOrderedBlock, order: number, description = 'change order') {
|
||||
const newBlock = new MutableOrderedBlock(block)
|
||||
newBlock.order = order
|
||||
@ -484,42 +500,69 @@ class Mutator {
|
||||
|
||||
// Duplicate
|
||||
|
||||
async duplicateCard(cardId: string, description = 'duplicate card', afterRedo?: (newBoardId: string) => Promise<void>, beforeUndo?: () => Promise<void>): Promise<[IBlock[], string]> {
|
||||
async duplicateCard(
|
||||
cardId: string,
|
||||
description = 'duplicate card',
|
||||
asTemplate = false,
|
||||
afterRedo?: (newCardId: string) => Promise<void>,
|
||||
beforeUndo?: () => Promise<void>,
|
||||
): Promise<[IBlock[], string]> {
|
||||
const blocks = await octoClient.getSubtree(cardId, 2)
|
||||
const [newBlocks1, idMap] = OctoUtils.duplicateBlockTree(blocks, cardId)
|
||||
const [newBlocks1, newCard] = OctoUtils.duplicateBlockTree(blocks, cardId) as [IBlock[], MutableCard, Record<string, string>]
|
||||
const newBlocks = newBlocks1.filter((o) => o.type !== 'comment')
|
||||
Utils.log(`duplicateCard: duplicating ${newBlocks.length} blocks`)
|
||||
const newCardId = idMap[cardId]
|
||||
const newCard = newBlocks.find((o) => o.id === newCardId)!
|
||||
if (asTemplate === newCard.isTemplate) {
|
||||
newCard.title = `Copy of ${newCard.title}`
|
||||
} else if (asTemplate) {
|
||||
// Template from card
|
||||
newCard.title = 'New card template'
|
||||
} else {
|
||||
// Card from template
|
||||
newCard.title = ''
|
||||
}
|
||||
newCard.isTemplate = asTemplate
|
||||
await this.insertBlocks(
|
||||
newBlocks,
|
||||
description,
|
||||
async () => {
|
||||
await afterRedo?.(newCardId)
|
||||
await afterRedo?.(newCard.id)
|
||||
},
|
||||
beforeUndo,
|
||||
)
|
||||
return [newBlocks, newCardId]
|
||||
return [newBlocks, newCard.id]
|
||||
}
|
||||
|
||||
async duplicateBoard(boardId: string, description = 'duplicate board', afterRedo?: (newBoardId: string) => Promise<void>, beforeUndo?: () => Promise<void>): Promise<[IBlock[], string]> {
|
||||
async duplicateBoard(
|
||||
boardId: string,
|
||||
description = 'duplicate board',
|
||||
asTemplate = false,
|
||||
afterRedo?: (newBoardId: string) => Promise<void>,
|
||||
beforeUndo?: () => Promise<void>,
|
||||
): Promise<[IBlock[], string]> {
|
||||
const blocks = await octoClient.getSubtree(boardId, 3)
|
||||
const [newBlocks1, idMap] = OctoUtils.duplicateBlockTree(blocks, boardId)
|
||||
const [newBlocks1, newBoard] = OctoUtils.duplicateBlockTree(blocks, boardId) as [IBlock[], MutableBoard, Record<string, string>]
|
||||
const newBlocks = newBlocks1.filter((o) => o.type !== 'comment')
|
||||
Utils.log(`duplicateBoard: duplicating ${newBlocks.length} blocks`)
|
||||
const newBoardId = idMap[boardId]
|
||||
const newBoard = newBlocks.find((o) => o.id === newBoardId)!
|
||||
|
||||
if (asTemplate === newBoard.isTemplate) {
|
||||
newBoard.title = `Copy of ${newBoard.title}`
|
||||
} else if (asTemplate) {
|
||||
// Template from board
|
||||
newBoard.title = 'New board template'
|
||||
} else {
|
||||
// Board from template
|
||||
newBoard.title = ''
|
||||
}
|
||||
newBoard.isTemplate = asTemplate
|
||||
await this.insertBlocks(
|
||||
newBlocks,
|
||||
description,
|
||||
async () => {
|
||||
await afterRedo?.(newBoardId)
|
||||
await afterRedo?.(newBoard.id)
|
||||
},
|
||||
beforeUndo,
|
||||
)
|
||||
return [newBlocks, newBoardId]
|
||||
return [newBlocks, newBoard.id]
|
||||
}
|
||||
|
||||
// Other methods
|
||||
|
@ -88,7 +88,7 @@ class OctoUtils {
|
||||
}
|
||||
|
||||
// Creates a copy of the blocks with new ids and parentIDs
|
||||
static duplicateBlockTree(blocks: IBlock[], rootBlockId?: string): [MutableBlock[], Readonly<Record<string, string>>] {
|
||||
static duplicateBlockTree(blocks: IBlock[], rootBlockId: string): [MutableBlock[], MutableBlock, Readonly<Record<string, string>>] {
|
||||
const idMap: Record<string, string> = {}
|
||||
const newBlocks = blocks.map((block) => {
|
||||
const newBlock = this.hydrateBlock(block)
|
||||
@ -97,7 +97,7 @@ class OctoUtils {
|
||||
return newBlock
|
||||
})
|
||||
|
||||
const newRootBlockId = rootBlockId ? idMap[rootBlockId] : undefined
|
||||
const newRootBlockId = idMap[rootBlockId]
|
||||
newBlocks.forEach((newBlock) => {
|
||||
// Note: Don't remap the parent of the new root block
|
||||
if (newBlock.id !== newRootBlockId && newBlock.parentId) {
|
||||
@ -112,7 +112,8 @@ class OctoUtils {
|
||||
}
|
||||
})
|
||||
|
||||
return [newBlocks, idMap]
|
||||
const newRootBlock = newBlocks.find((block) => block.id === newRootBlockId)!
|
||||
return [newBlocks, newRootBlock, idMap]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,7 +143,7 @@ export default class BoardPage extends React.Component<Props, State> {
|
||||
|
||||
const workspaceTree = new MutableWorkspaceTree()
|
||||
await workspaceTree.sync()
|
||||
const boardIds = workspaceTree.boards.map((o) => o.id)
|
||||
const boardIds = [...workspaceTree.boards.map((o) => o.id), ...workspaceTree.boardTemplates.map((o) => o.id)]
|
||||
this.setState({workspaceTree})
|
||||
|
||||
// Listen to boards plus all blocks at root (Empty string for parentId)
|
||||
|
@ -100,6 +100,10 @@ hr {
|
||||
overflow: scroll;
|
||||
|
||||
padding: 10px 95px 50px 95px;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
padding: 10px 10px 50px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dragover {
|
||||
@ -173,6 +177,7 @@ hr {
|
||||
}
|
||||
|
||||
.octo-block img {
|
||||
width: calc(100% - 20px);
|
||||
max-width: 500px;
|
||||
max-height: 500px;
|
||||
margin: 5px 0;
|
||||
@ -189,7 +194,13 @@ hr {
|
||||
align-items: flex-start;
|
||||
|
||||
width: 100%;
|
||||
|
||||
@media not screen and (max-width: 975px) {
|
||||
padding-right: 126px;
|
||||
}
|
||||
@media screen and (max-width: 975px) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
> * {
|
||||
flex: 1 1 auto;
|
||||
@ -207,5 +218,7 @@ hr {
|
||||
padding-top: 10px;
|
||||
padding-right: 10px;
|
||||
|
||||
@media not screen and (max-width: 975px) {
|
||||
width: 126px;
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import {Utils} from './utils'
|
||||
test('Basic undo/redo', async () => {
|
||||
expect(undoManager.canUndo).toBe(false)
|
||||
expect(undoManager.canRedo).toBe(false)
|
||||
expect(undoManager.currentCheckpoint).toBe(0)
|
||||
|
||||
const values: string[] = []
|
||||
|
||||
@ -22,6 +23,7 @@ test('Basic undo/redo', async () => {
|
||||
|
||||
expect(undoManager.canUndo).toBe(true)
|
||||
expect(undoManager.canRedo).toBe(false)
|
||||
expect(undoManager.currentCheckpoint).toBeGreaterThan(0)
|
||||
expect(Utils.arraysEqual(values, ['a'])).toBe(true)
|
||||
expect(undoManager.undoDescription).toBe('test')
|
||||
expect(undoManager.redoDescription).toBe(undefined)
|
||||
@ -41,6 +43,7 @@ test('Basic undo/redo', async () => {
|
||||
await undoManager.clear()
|
||||
expect(undoManager.canUndo).toBe(false)
|
||||
expect(undoManager.canRedo).toBe(false)
|
||||
expect(undoManager.currentCheckpoint).toBe(0)
|
||||
expect(undoManager.undoDescription).toBe(undefined)
|
||||
expect(undoManager.redoDescription).toBe(undefined)
|
||||
})
|
||||
|
@ -49,7 +49,7 @@ class Utils {
|
||||
// HACKHACK: Somehow, marked doesn't encode angle brackets
|
||||
const renderer = new marked.Renderer()
|
||||
renderer.link = (href, title, contents) => `<a target="_blank" href="${href}" title="${title || ''}" onclick="event.stopPropagation();">${contents}</a>`
|
||||
const html = marked(text.replace(/</g, '<'), {renderer})
|
||||
const html = marked(text.replace(/</g, '<'), {renderer, breaks: true})
|
||||
return html
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IBlock, MutableBlock} from '../blocks/block'
|
||||
import {IBlock} from '../blocks/block'
|
||||
import {Card, MutableCard} from '../blocks/card'
|
||||
import {IOrderedBlock} from '../blocks/orderedBlock'
|
||||
import octoClient from '../octoClient'
|
||||
@ -12,7 +12,6 @@ interface CardTree {
|
||||
readonly contents: readonly IOrderedBlock[]
|
||||
|
||||
mutableCopy(): MutableCardTree
|
||||
templateCopy(): MutableCardTree
|
||||
}
|
||||
|
||||
class MutableCardTree implements CardTree {
|
||||
@ -56,20 +55,6 @@ class MutableCardTree implements CardTree {
|
||||
cardTree.incrementalUpdate(this.rawBlocks)
|
||||
return cardTree
|
||||
}
|
||||
|
||||
templateCopy(): MutableCardTree {
|
||||
const card = this.card.duplicate()
|
||||
|
||||
const contents: IOrderedBlock[] = this.contents.map((content) => {
|
||||
const copy = MutableBlock.duplicate(content)
|
||||
copy.parentId = card.id
|
||||
return copy as IOrderedBlock
|
||||
})
|
||||
|
||||
const cardTree = new MutableCardTree(card.id)
|
||||
cardTree.incrementalUpdate([card, ...contents])
|
||||
return cardTree
|
||||
}
|
||||
}
|
||||
|
||||
export {MutableCardTree, CardTree}
|
||||
|
@ -8,6 +8,7 @@ import {OctoUtils} from '../octoUtils'
|
||||
|
||||
interface WorkspaceTree {
|
||||
readonly boards: readonly Board[]
|
||||
readonly boardTemplates: readonly Board[]
|
||||
readonly views: readonly BoardView[]
|
||||
|
||||
mutableCopy(): MutableWorkspaceTree
|
||||
@ -15,6 +16,7 @@ interface WorkspaceTree {
|
||||
|
||||
class MutableWorkspaceTree {
|
||||
boards: Board[] = []
|
||||
boardTemplates: Board[] = []
|
||||
views: BoardView[] = []
|
||||
|
||||
private rawBlocks: IBlock[] = []
|
||||
@ -37,7 +39,10 @@ class MutableWorkspaceTree {
|
||||
}
|
||||
|
||||
private rebuild(blocks: IBlock[]) {
|
||||
this.boards = blocks.filter((block) => block.type === 'board').
|
||||
const allBoards = blocks.filter((block) => block.type === 'board') as Board[]
|
||||
this.boards = allBoards.filter((block) => !block.isTemplate).
|
||||
sort((a, b) => a.title.localeCompare(b.title)) as Board[]
|
||||
this.boardTemplates = allBoards.filter((block) => block.isTemplate).
|
||||
sort((a, b) => a.title.localeCompare(b.title)) as Board[]
|
||||
this.views = blocks.filter((block) => block.type === 'view').
|
||||
sort((a, b) => a.title.localeCompare(b.title)) as BoardView[]
|
||||
|
@ -1,7 +1,6 @@
|
||||
.IconButton {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background-color: rgba(var(--main-fg), 0.1);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.Icon {
|
||||
|
@ -10,7 +10,7 @@ type Props = {
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
export default class Button extends React.PureComponent<Props> {
|
||||
export default class IconButton extends React.PureComponent<Props> {
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
|
@ -24,16 +24,16 @@ export default class Editable extends React.Component<Props> {
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.elementRef.current!.focus()
|
||||
|
||||
// Put cursor at end
|
||||
document.execCommand('selectAll', false, undefined)
|
||||
document.getSelection()?.collapseToEnd()
|
||||
if (this.elementRef.current) {
|
||||
const valueLength = this.elementRef.current.value.length
|
||||
this.elementRef.current.focus()
|
||||
this.elementRef.current.setSelectionRange(valueLength, valueLength)
|
||||
}
|
||||
}
|
||||
|
||||
public blur = (): void => {
|
||||
this.saveOnBlur = false
|
||||
this.elementRef.current!.blur()
|
||||
this.elementRef.current?.blur()
|
||||
this.saveOnBlur = true
|
||||
}
|
||||
|
||||
|
7
webapp/src/widgets/icons/close.scss
Normal file
7
webapp/src/widgets/icons/close.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.CloseIcon {
|
||||
stroke: rgb(var(--main-fg), 0.5);
|
||||
stroke-width: 4px;
|
||||
fill: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
19
webapp/src/widgets/icons/close.tsx
Normal file
19
webapp/src/widgets/icons/close.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import './close.scss'
|
||||
|
||||
export default function CloseIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='CloseIcon Icon'
|
||||
viewBox='0 0 100 100'
|
||||
>
|
||||
<polyline points='30,30 70,70'/>
|
||||
<polyline points='70,30 30,70'/>
|
||||
</svg>
|
||||
)
|
||||
}
|
@ -33,32 +33,34 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
padding: 2px 10px;
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
|
||||
* {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(90, 90, 90, 0.1);
|
||||
}
|
||||
|
||||
.menu-name {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.SubmenuTriangleIcon {
|
||||
fill: rgba(var(--main-fg), 0.7);
|
||||
}
|
||||
|
||||
.Icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.IconButton .Icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user