diff --git a/webapp/src/blocks/card.ts b/webapp/src/blocks/card.ts index 02721cbda..622a6d732 100644 --- a/webapp/src/blocks/card.ts +++ b/webapp/src/blocks/card.ts @@ -7,7 +7,7 @@ import {IBlock, MutableBlock} from './block' interface Card extends IBlock { readonly icon: string readonly isTemplate: boolean - readonly properties: Readonly> + readonly properties: Readonly> readonly contentOrder: readonly string[] duplicate(): MutableCard @@ -28,10 +28,10 @@ class MutableCard extends MutableBlock implements Card { this.fields.isTemplate = value } - get properties(): Record { - return this.fields.properties as Record + get properties(): Record { + return this.fields.properties as Record } - set properties(value: Record) { + set properties(value: Record) { this.fields.properties = value } diff --git a/webapp/src/components/cardDetail/cardDetail.scss b/webapp/src/components/cardDetail/cardDetail.scss index 59049a333..ff4bb05a1 100644 --- a/webapp/src/components/cardDetail/cardDetail.scss +++ b/webapp/src/components/cardDetail/cardDetail.scss @@ -23,6 +23,7 @@ width: 100%; .MenuWrapper { position: relative; + align-self: center; } } diff --git a/webapp/src/components/properties/multiSelect.tsx b/webapp/src/components/properties/multiSelect.tsx new file mode 100644 index 000000000..86ab3bef9 --- /dev/null +++ b/webapp/src/components/properties/multiSelect.tsx @@ -0,0 +1,64 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react' + +import {IPropertyOption, IPropertyTemplate} from '../../blocks/board' + +import Label from '../../widgets/label' + +import ValueSelector from '../../widgets/valueSelector' + +type Props = { + emptyValue: string; + propertyTemplate: IPropertyTemplate; + propertyValue: string | string[]; + onChange: (value: string | string[]) => void; + onChangeColor: (option: IPropertyOption, color: string) => void; + onDeleteOption: (option: IPropertyOption) => void; + onCreate: (newValue: string, currentValues: IPropertyOption[]) => void; + onDeleteValue: (valueToDelete: IPropertyOption, currentValues: IPropertyOption[]) => void; + isEditable: boolean; +} + +const MultiSelectProperty = (props: Props): JSX.Element => { + const {propertyTemplate, emptyValue, propertyValue, isEditable, onChange, onChangeColor, onDeleteOption, onCreate, onDeleteValue} = props + + const values = Array.isArray(propertyValue) ? + propertyValue.map((v) => propertyTemplate.options.find((o) => o!.id === v)).filter((v): v is IPropertyOption => Boolean(v)) : + [] + + if (!isEditable) { + return ( +
+ {values.map((v) => ( + + ))} +
+ ) + } + + return ( + onDeleteValue(valueToRemove, values)} + onCreate={(newValue) => onCreate(newValue, values)} + /> + ) +} + +export default MultiSelectProperty diff --git a/webapp/src/components/propertyValueElement.tsx b/webapp/src/components/propertyValueElement.tsx index f35813836..26d3cbd98 100644 --- a/webapp/src/components/propertyValueElement.tsx +++ b/webapp/src/components/propertyValueElement.tsx @@ -12,11 +12,13 @@ import {Utils} from '../utils' import {BoardTree} from '../viewModel/boardTree' import Editable from '../widgets/editable' import ValueSelector from '../widgets/valueSelector' + import Label from '../widgets/label' import EditableDayPicker from '../widgets/editableDayPicker' import Switch from '../widgets/switch' +import MultiSelectProperty from './properties/multiSelect' import URLProperty from './properties/link/link' type Props = { @@ -60,6 +62,33 @@ const PropertyValueElement = (props:Props): JSX.Element => { } } + if (propertyTemplate.type === 'multiSelect') { + return ( + mutator.changePropertyValue(card, propertyTemplate.id, newValue)} + onChangeColor={(option: IPropertyOption, colorId: string) => mutator.changePropertyOptionColor(boardTree!.board, propertyTemplate, option, colorId)} + onDeleteOption={(option: IPropertyOption) => mutator.deletePropertyOption(boardTree!, propertyTemplate, option)} + onCreate={ + async (newValue, currentValues) => { + const option: IPropertyOption = { + id: Utils.createGuid(), + value: newValue, + color: 'propColorDefault', + } + currentValues.push(option) + await mutator.insertPropertyOption(boardTree!, propertyTemplate, option, 'add property option') + mutator.changePropertyValue(card, propertyTemplate.id, currentValues.map((v) => v.id)) + } + } + onDeleteValue={(valueToDelete, currentValues) => mutator.changePropertyValue(card, propertyTemplate.id, currentValues.filter((currentValue) => currentValue.id !== valueToDelete.id).map((currentValue) => currentValue.id))} + /> + ) + } + if (propertyTemplate.type === 'select') { let propertyColorCssClassName = '' const cardPropertyValue = propertyTemplate.options.find((o) => o.id === propertyValue) @@ -107,7 +136,7 @@ const PropertyValueElement = (props:Props): JSX.Element => { } else if (propertyTemplate.type === 'url') { return ( mutator.changePropertyValue(card, propertyTemplate.id, value)} onCancel={() => setValue(propertyValue)} @@ -123,7 +152,7 @@ const PropertyValueElement = (props:Props): JSX.Element => { return ( mutator.changePropertyValue(card, propertyTemplate.id, newValue)} /> ) @@ -152,7 +181,7 @@ const PropertyValueElement = (props:Props): JSX.Element => { mutator.changePropertyValue(card, propertyTemplate.id, value)} onCancel={() => setValue(propertyValue)} diff --git a/webapp/src/components/table/table.scss b/webapp/src/components/table/table.scss index 2d7f903cc..a85925d14 100644 --- a/webapp/src/components/table/table.scss +++ b/webapp/src/components/table/table.scss @@ -9,7 +9,7 @@ padding: 8px; min-height: 32px; font-size: 14px; - line-height: 20px; + line-height: 30px; position: relative; text-overflow: ellipsis; diff --git a/webapp/src/components/table/table.tsx b/webapp/src/components/table/table.tsx index b76371519..51c3a7ba1 100644 --- a/webapp/src/components/table/table.tsx +++ b/webapp/src/components/table/table.tsx @@ -82,7 +82,7 @@ const Table = (props: Props) => { return } - displayValue = OctoUtils.propertyDisplayValue(card, card.properties[columnID], template!, props.intl) || '' + displayValue = (OctoUtils.propertyDisplayValue(card, card.properties[columnID], template!, props.intl) || '') as string if (template.type === 'select') { displayValue = displayValue.toUpperCase() } diff --git a/webapp/src/csvExporter.ts b/webapp/src/csvExporter.ts index dcfc766d9..1dae75652 100644 --- a/webapp/src/csvExporter.ts +++ b/webapp/src/csvExporter.ts @@ -67,7 +67,7 @@ class CsvExporter { row.push(`"${this.encodeText(card.title)}"`) visibleProperties.forEach((template) => { const propertyValue = card.properties[template.id] - const displayValue = OctoUtils.propertyDisplayValue(card, propertyValue, template, intl) || '' + const displayValue = (OctoUtils.propertyDisplayValue(card, propertyValue, template, intl) || '') as string if (template.type === 'number') { const numericValue = propertyValue ? Number(propertyValue).toString() : '' row.push(numericValue) diff --git a/webapp/src/mutator.ts b/webapp/src/mutator.ts index e9e666f73..405645561 100644 --- a/webapp/src/mutator.ts +++ b/webapp/src/mutator.ts @@ -372,7 +372,7 @@ class Mutator { await this.updateBlock(newBoard, board, 'change option color') } - async changePropertyValue(card: Card, propertyId: string, value?: string, description = 'change property') { + async changePropertyValue(card: Card, propertyId: string, value?: string | string[], description = 'change property') { const newCard = new MutableCard(card) if (value) { newCard.properties[propertyId] = value @@ -383,6 +383,10 @@ class Mutator { } async changePropertyType(boardTree: BoardTree, propertyTemplate: IPropertyTemplate, type: PropertyType) { + if (propertyTemplate.type === type) { + return + } + const {board} = boardTree const newBoard = new MutableBoard(board) @@ -392,27 +396,40 @@ class Mutator { const oldBlocks: IBlock[] = [board] const newBlocks: IBlock[] = [newBoard] - if (propertyTemplate.type === 'select') { - // Map select to their values + + if (propertyTemplate.type === 'select' || propertyTemplate.type === 'multiSelect') { // If the old type was either select or multiselect + const isNewTypeSelectOrMulti = type === 'select' || type === 'multiSelect' + for (const card of boardTree.allCards) { - const oldValue = card.properties[propertyTemplate.id] + const oldValue = Array.isArray(card.properties[propertyTemplate.id]) ? + (card.properties[propertyTemplate.id].length > 0 && card.properties[propertyTemplate.id][0]) : + card.properties[propertyTemplate.id] + if (oldValue) { - const newValue = propertyTemplate.options.find((o) => o.id === oldValue)?.value + const newValue = isNewTypeSelectOrMulti ? + propertyTemplate.options.find((o) => o.id === oldValue)?.id : + propertyTemplate.options.find((o) => o.id === oldValue)?.value const newCard = new MutableCard(card) + if (newValue) { - newCard.properties[propertyTemplate.id] = newValue + newCard.properties[propertyTemplate.id] = type === 'multiSelect' ? [newValue] : newValue } else { // This was an invalid select option, so delete it delete newCard.properties[propertyTemplate.id] } + newBlocks.push(newCard) oldBlocks.push(card) } + + if (isNewTypeSelectOrMulti) { + newTemplate.options = propertyTemplate.options + } } - } else if (type === 'select') { + } else if (type === 'select' || type === 'multiSelect') { // if the new type is either select or multiselect // Map values to new template option IDs for (const card of boardTree.allCards) { - const oldValue = card.properties[propertyTemplate.id] + const oldValue = card.properties[propertyTemplate.id] as string if (oldValue) { let option = newTemplate.options.find((o) => o.value === oldValue) if (!option) { @@ -425,7 +442,7 @@ class Mutator { } const newCard = new MutableCard(card) - newCard.properties[propertyTemplate.id] = option.id + newCard.properties[propertyTemplate.id] = type === 'multiSelect' ? [option.id] : option.id newBlocks.push(newCard) oldBlocks.push(card) diff --git a/webapp/src/octoUtils.tsx b/webapp/src/octoUtils.tsx index 468387f8a..80013c9a9 100644 --- a/webapp/src/octoUtils.tsx +++ b/webapp/src/octoUtils.tsx @@ -16,8 +16,8 @@ import {FilterCondition} from './blocks/filterClause' import {Utils} from './utils' class OctoUtils { - static propertyDisplayValue(block: IBlock, propertyValue: string | undefined, propertyTemplate: IPropertyTemplate, intl: IntlShape): string | undefined { - let displayValue: string | undefined + static propertyDisplayValue(block: IBlock, propertyValue: string | string[] | undefined, propertyTemplate: IPropertyTemplate, intl: IntlShape): string | string[] | undefined { + let displayValue: string | string[] | undefined switch (propertyTemplate.type) { case 'select': { // The property value is the id of the template @@ -40,7 +40,7 @@ class OctoUtils { } case 'date': { if (propertyValue) { - displayValue = Utils.displayDate(new Date(parseInt(propertyValue, 10)), intl) + displayValue = Utils.displayDate(new Date(parseInt(propertyValue as string, 10)), intl) } break } diff --git a/webapp/src/viewModel/boardTree.ts b/webapp/src/viewModel/boardTree.ts index 1b2eb337d..cc07aa28a 100644 --- a/webapp/src/viewModel/boardTree.ts +++ b/webapp/src/viewModel/boardTree.ts @@ -216,7 +216,9 @@ class MutableBoardTree implements BoardTree { if (option?.value.toLowerCase().includes(searchText)) { return true } - } else if (propertyValue.toLowerCase().includes(searchText)) { + + // TODO: Add search capability for multi-select values BIG BOYY + } else if ((propertyValue as string).toLowerCase().includes(searchText)) { return true } } @@ -425,7 +427,7 @@ class MutableBoardTree implements BoardTree { return this.titleOrCreatedOrder(a, b) } - result = aValue.localeCompare(bValue) + result = (aValue as string).localeCompare(bValue as string) } if (result === 0) { diff --git a/webapp/src/widgets/__snapshots__/propertyMenu.test.tsx.snap b/webapp/src/widgets/__snapshots__/propertyMenu.test.tsx.snap index 00a633829..ffbc142ba 100644 --- a/webapp/src/widgets/__snapshots__/propertyMenu.test.tsx.snap +++ b/webapp/src/widgets/__snapshots__/propertyMenu.test.tsx.snap @@ -157,6 +157,21 @@ exports[`widgets/PropertyMenu should match snapshot 1`] = ` class="noicon" /> +