diff --git a/webapp/src/components/__snapshots__/propertyValueElement.test.tsx.snap b/webapp/src/components/__snapshots__/propertyValueElement.test.tsx.snap index 5861999df..ac92b43a0 100644 --- a/webapp/src/components/__snapshots__/propertyValueElement.test.tsx.snap +++ b/webapp/src/components/__snapshots__/propertyValueElement.test.tsx.snap @@ -56,6 +56,7 @@ exports[`components/propertyValueElement should match snapshot, select 1`] = `
value 1 -
- -
@@ -85,6 +76,7 @@ exports[`components/propertyValueElement should match snapshot, select, read-onl
Jean-Luc Picard -
- -
diff --git a/webapp/src/components/kanban/kanbanCard.scss b/webapp/src/components/kanban/kanbanCard.scss index c723985a1..09d1c1e0f 100644 --- a/webapp/src/components/kanban/kanbanCard.scss +++ b/webapp/src/components/kanban/kanbanCard.scss @@ -35,7 +35,7 @@ right: 0; } - > .octo-propertyvalue { + .octo-propertyvalue { margin: 4px 0 0; font-size: 12px; line-height: 18px; diff --git a/webapp/src/components/properties/__snapshots__/multiSelect.test.tsx.snap b/webapp/src/components/properties/multiSelect/__snapshots__/multiSelect.test.tsx.snap similarity index 100% rename from webapp/src/components/properties/__snapshots__/multiSelect.test.tsx.snap rename to webapp/src/components/properties/multiSelect/__snapshots__/multiSelect.test.tsx.snap diff --git a/webapp/src/components/properties/multiSelect.test.tsx b/webapp/src/components/properties/multiSelect/multiSelect.test.tsx similarity index 99% rename from webapp/src/components/properties/multiSelect.test.tsx rename to webapp/src/components/properties/multiSelect/multiSelect.test.tsx index a3cb4d5e3..ffe55c961 100644 --- a/webapp/src/components/properties/multiSelect.test.tsx +++ b/webapp/src/components/properties/multiSelect/multiSelect.test.tsx @@ -6,7 +6,7 @@ import userEvent from '@testing-library/user-event' import '@testing-library/jest-dom' import {IntlProvider} from 'react-intl' -import {IPropertyOption, IPropertyTemplate} from '../../blocks/board' +import {IPropertyOption, IPropertyTemplate} from '../../../blocks/board' import MultiSelect from './multiSelect' diff --git a/webapp/src/components/properties/multiSelect.tsx b/webapp/src/components/properties/multiSelect/multiSelect.tsx similarity index 87% rename from webapp/src/components/properties/multiSelect.tsx rename to webapp/src/components/properties/multiSelect/multiSelect.tsx index 5579014f7..a31b41b1f 100644 --- a/webapp/src/components/properties/multiSelect.tsx +++ b/webapp/src/components/properties/multiSelect/multiSelect.tsx @@ -3,11 +3,11 @@ import React, {useState} from 'react' -import {IPropertyOption, IPropertyTemplate} from '../../blocks/board' +import {IPropertyOption, IPropertyTemplate} from '../../../blocks/board' -import Label from '../../widgets/label' +import Label from '../../../widgets/label' -import ValueSelector from '../../widgets/valueSelector' +import ValueSelector from '../../../widgets/valueSelector' type Props = { emptyValue: string; @@ -44,7 +44,9 @@ const MultiSelectProperty = (props: Props): JSX.Element => { ))} {values.length === 0 && ( - + )}
) @@ -61,6 +63,7 @@ const MultiSelectProperty = (props: Props): JSX.Element => { onDeleteOption={onDeleteOption} onDeleteValue={(valueToRemove) => onDeleteValue(valueToRemove, values)} onCreate={(newValue) => onCreate(newValue, values)} + onBlur={() => setOpen(false)} /> ) } diff --git a/webapp/src/components/properties/select/__snapshots__/select.test.tsx.snap b/webapp/src/components/properties/select/__snapshots__/select.test.tsx.snap new file mode 100644 index 000000000..b45c105db --- /dev/null +++ b/webapp/src/components/properties/select/__snapshots__/select.test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/properties/select shows empty placeholder 1`] = ` +
+
+ + + Empty + + +
+
+`; + +exports[`components/properties/select shows the selected option 1`] = ` +
+
+ + + one + + +
+
+`; diff --git a/webapp/src/components/properties/select/select.test.tsx b/webapp/src/components/properties/select/select.test.tsx new file mode 100644 index 000000000..2eb61f334 --- /dev/null +++ b/webapp/src/components/properties/select/select.test.tsx @@ -0,0 +1,193 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react' +import {render, screen} from '@testing-library/react' +import '@testing-library/jest-dom' + +import userEvent from '@testing-library/user-event' + +import {IPropertyTemplate} from '../../../blocks/board' + +import {wrapIntl} from '../../../testUtils' + +import Select from './select' + +function selectPropertyTemplate(): IPropertyTemplate { + return { + id: 'select-template', + name: 'select', + type: 'select', + options: [ + { + id: 'option-1', + value: 'one', + color: 'propColorDefault', + }, + { + id: 'option-2', + value: 'two', + color: 'propColorGreen', + }, + { + id: 'option-3', + value: 'three', + color: 'propColorRed', + }, + ], + } +} + +function selectCallbacks() { + return { + onCreate: jest.fn(), + onChange: jest.fn(), + onChangeColor: jest.fn(), + onDeleteOption: jest.fn(), + onDeleteValue: jest.fn(), + } +} + +describe('components/properties/select', () => { + const nonEditableSelectTestId = 'select-non-editable' + + const clearButton = () => screen.queryByRole('button', {name: /clear/i}) + + it('shows the selected option', () => { + const propertyTemplate = selectPropertyTemplate() + const option = propertyTemplate.options[0] + + const {container} = render(wrapIntl( + , + )) + + expect(screen.getByText(emptyValue)).toBeInTheDocument() + expect(clearButton()).not.toBeInTheDocument() + + expect(container).toMatchSnapshot() + }) + + it('shows the menu with options when preview is clicked', () => { + const propertyTemplate = selectPropertyTemplate() + const selected = propertyTemplate.options[1] + + render(wrapIntl( + , + )) + + userEvent.click(screen.getByTestId(nonEditableSelectTestId)) + userEvent.click(screen.getByText(optionToSelect.value)) + + expect(clearButton()).not.toBeInTheDocument() + expect(onChange).toHaveBeenCalledWith(optionToSelect.id) + }) + + it('can clear the selected option', () => { + const propertyTemplate = selectPropertyTemplate() + const selected = propertyTemplate.options[1] + const onDeleteValue = jest.fn() + + render(wrapIntl( + , + )) + + userEvent.click(screen.getByTestId(nonEditableSelectTestId)) + userEvent.type(screen.getByRole('textbox', {name: /value selector/i}), `${newOption}{enter}`) + + expect(onCreate).toHaveBeenCalledWith(newOption) + }) +}) diff --git a/webapp/src/components/properties/select/select.tsx b/webapp/src/components/properties/select/select.tsx new file mode 100644 index 000000000..8560e7838 --- /dev/null +++ b/webapp/src/components/properties/select/select.tsx @@ -0,0 +1,61 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useState} from 'react' + +import {IPropertyOption, IPropertyTemplate} from '../../../blocks/board' + +import Label from '../../../widgets/label' +import ValueSelector from '../../../widgets/valueSelector' + +type Props = { + emptyValue: string + propertyValue: string + propertyTemplate: IPropertyTemplate + onCreate: (value: string) => void + onChange: (value: string) => void + onChangeColor: (option: IPropertyOption, color: string) => void + onDeleteOption: (option: IPropertyOption) => void + onDeleteValue: () => void; + isEditable: boolean +} + +const SelectProperty = React.memo((props: Props) => { + const {emptyValue, propertyValue, propertyTemplate, isEditable} = props + const [open, setOpen] = useState(false) + + const option = propertyTemplate.options.find((o) => o.id === propertyValue) + const propertyColorCssClassName = option?.color || '' + const displayValue = option?.value + const finalDisplayValue = displayValue || emptyValue + + if (!isEditable || !open) { + return ( +
setOpen(true)} + > + +
+ ) + } + return ( + p.id === propertyValue)} + onCreate={props.onCreate} + onChange={(value) => props.onChange(value as string)} + onChangeColor={props.onChangeColor} + onDeleteOption={props.onDeleteOption} + onDeleteValue={props.onDeleteValue} + onBlur={() => setOpen(false)} + /> + ) +}) + +export default SelectProperty diff --git a/webapp/src/components/propertyValueElement.tsx b/webapp/src/components/propertyValueElement.tsx index c744f193c..356048522 100644 --- a/webapp/src/components/propertyValueElement.tsx +++ b/webapp/src/components/propertyValueElement.tsx @@ -12,22 +12,17 @@ import mutator from '../mutator' import {OctoUtils} from '../octoUtils' import {Utils} from '../utils' import Editable from '../widgets/editable' -import ValueSelector from '../widgets/valueSelector' - -import Label from '../widgets/label' - import Switch from '../widgets/switch' -import IconButton from '../widgets/buttons/iconButton' -import CloseIcon from '../widgets/icons/close' import UserProperty from './properties/user/user' -import MultiSelectProperty from './properties/multiSelect' +import MultiSelectProperty from './properties/multiSelect/multiSelect' import URLProperty from './properties/link/link' import LastModifiedBy from './properties/lastModifiedBy/lastModifiedBy' import LastModifiedAt from './properties/lastModifiedAt/lastModifiedAt' import CreatedAt from './properties/createdAt/createdAt' import CreatedBy from './properties/createdBy/createdBy' import DateRange from './properties/dateRange/dateRange' +import SelectProperty from './properties/select/select' type Props = { board: Board @@ -48,7 +43,6 @@ const PropertyValueElement = (props:Props): JSX.Element => { const propertyValue = card.fields.properties[propertyTemplate.id] const displayValue = OctoUtils.propertyDisplayValue(card, propertyValue, propertyTemplate, intl) const finalDisplayValue = displayValue || emptyDisplayValue - const [open, setOpen] = useState(false) const editableFields: Array = ['text', 'number', 'email', 'url', 'phone'] @@ -130,47 +124,12 @@ const PropertyValueElement = (props:Props): JSX.Element => { } if (propertyTemplate.type === 'select') { - let propertyColorCssClassName = '' - const cardPropertyValue = propertyTemplate.options.find((o) => o.id === propertyValue) - if (cardPropertyValue) { - propertyColorCssClassName = cardPropertyValue.color - } - - if (readOnly || !board || !open) { - return ( -
setOpen(true)} - > - -
- ) - } return ( - p.id === propertyValue)} - onChange={(newValue) => { - mutator.changePropertyValue(card, propertyTemplate.id, newValue) - }} - onChangeColor={(option: IPropertyOption, colorId: string): void => { - mutator.changePropertyOptionColor(board, propertyTemplate, option, colorId) - }} - onDeleteOption={(option: IPropertyOption): void => { - mutator.deletePropertyOption(board, propertyTemplate, option) - }} + propertyValue={propertyValue as string} + propertyTemplate={propertyTemplate} onCreate={ async (newValue) => { const option: IPropertyOption = { @@ -182,6 +141,15 @@ const PropertyValueElement = (props:Props): JSX.Element => { mutator.changePropertyValue(card, propertyTemplate.id, option.id) } } + onChange={(newValue) => { + mutator.changePropertyValue(card, propertyTemplate.id, newValue) + }} + onChangeColor={(option: IPropertyOption, colorId: string): void => { + mutator.changePropertyOptionColor(board, propertyTemplate, option, colorId) + }} + onDeleteOption={(option: IPropertyOption): void => { + mutator.deletePropertyOption(board, propertyTemplate, option) + }} onDeleteValue={onDeleteValue} /> ) diff --git a/webapp/src/components/table/__snapshots__/table.test.tsx.snap b/webapp/src/components/table/__snapshots__/table.test.tsx.snap index 46cfa9860..20a19a942 100644 --- a/webapp/src/components/table/__snapshots__/table.test.tsx.snap +++ b/webapp/src/components/table/__snapshots__/table.test.tsx.snap @@ -180,6 +180,7 @@ exports[`components/table/Table extended should match snapshot with CreatedBy 1` >
value 1 -
- -
@@ -209,6 +200,7 @@ exports[`components/table/Table extended should match snapshot with CreatedBy 1` >
value 1 -
- -
@@ -305,6 +288,7 @@ exports[`components/table/Table extended should match snapshot with CreatedBy 1` >
value 1 -
- -
@@ -613,6 +588,7 @@ exports[`components/table/Table extended should match snapshot with CreatedBy 2` >
value 1 -
- -
@@ -709,6 +676,7 @@ exports[`components/table/Table extended should match snapshot with CreatedBy 2` >
value 1 -
- -
@@ -1017,6 +976,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedAt 1` >
value 1 -
- -
@@ -1113,6 +1064,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedAt 1` >
value 1 -
- -
@@ -1421,6 +1364,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedBy 1` >
value 1 -
- -
@@ -1517,6 +1452,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedBy 1` >
value 1 -
- -
@@ -1801,6 +1728,7 @@ exports[`components/table/Table should match snapshot 1`] = ` >
value 1 -
- -
@@ -166,6 +157,7 @@ exports[`components/table/TableRow should match snapshot, display properties 1`] >
value 1 -
- -
@@ -348,6 +331,7 @@ exports[`components/table/TableRow should match snapshot, resizing column 1`] = >
{children} +export const wrapIntl = (children?: React.ReactNode): JSX.Element => {children} export function mockDOM(): void { window.focus = jest.fn() diff --git a/webapp/src/widgets/label.scss b/webapp/src/widgets/label.scss index 604f02f6d..fd97de044 100644 --- a/webapp/src/widgets/label.scss +++ b/webapp/src/widgets/label.scss @@ -2,6 +2,8 @@ display: inline-flex; align-items: center; padding: 2px 8px; + margin-right: 5px; + margin-bottom: 5px; border-radius: 3px; line-height: 20px; color: rgba(var(--center-channel-color-rgb), 0.8); @@ -9,8 +11,6 @@ text-transform: uppercase; font-weight: 600; font-size: 13px; - margin-right: 5px; - margin-bottom: 5px; input { line-height: 20px; diff --git a/webapp/src/widgets/label.tsx b/webapp/src/widgets/label.tsx index 5926bf25f..455c605d7 100644 --- a/webapp/src/widgets/label.tsx +++ b/webapp/src/widgets/label.tsx @@ -10,7 +10,7 @@ type Props = { color?: string title?: string children: React.ReactNode - classNames?: string + className?: string } // Switch is an on-off style switch / checkbox @@ -21,7 +21,7 @@ function Label(props: Props): JSX.Element { } return ( {props.children} diff --git a/webapp/src/widgets/valueSelector.scss b/webapp/src/widgets/valueSelector.scss index 4d719bd0b..d610d5b3b 100644 --- a/webapp/src/widgets/valueSelector.scss +++ b/webapp/src/widgets/valueSelector.scss @@ -36,6 +36,10 @@ padding-bottom: 0; } + .Label-no-margin { + margin: 0; + } + .Label-single-select { margin-bottom: 0; } diff --git a/webapp/src/widgets/valueSelector.tsx b/webapp/src/widgets/valueSelector.tsx index ff85b9b18..b5549ec6e 100644 --- a/webapp/src/widgets/valueSelector.tsx +++ b/webapp/src/widgets/valueSelector.tsx @@ -32,6 +32,7 @@ type Props = { onDeleteOption: (option: IPropertyOption) => void isMulti?: boolean onDeleteValue?: (value: IPropertyOption) => void + onBlur?: () => void } type LabelProps = { @@ -40,22 +41,26 @@ type LabelProps = { onChangeColor: (option: IPropertyOption, color: string) => void onDeleteOption: (option: IPropertyOption) => void onDeleteValue?: (value: IPropertyOption) => void + isMulti?: boolean } const ValueSelectorLabel = React.memo((props: LabelProps): JSX.Element => { - const {option, onDeleteValue, meta} = props + const {option, onDeleteValue, meta, isMulti} = props const intl = useIntl() if (meta.context === 'value') { + let className = onDeleteValue ? 'Label-no-padding' : 'Label-single-select' + if (!isMulti) { + className += ' Label-no-margin' + } return (