1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-23 18:34:02 +02:00

Added property to select multiperson (#3741)

* Added property to select multiperson

* Implemented the sorting for multiperson property

* Updated snapshots

* Added Test case

* CSS fix

* Linter fixes

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Rajat Dabade 2022-09-07 02:39:08 +05:30 committed by GitHub
parent a1a67390fc
commit ad3b8fd454
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 940 additions and 1 deletions

View File

@ -74,7 +74,7 @@ type BoardsAndBlocksPatch = {
blockPatches: BlockPatch[],
}
type PropertyTypeEnum = 'text' | 'number' | 'select' | 'multiSelect' | 'date' | 'person' | 'file' | 'checkbox' | 'url' | 'email' | 'phone' | 'createdTime' | 'createdBy' | 'updatedTime' | 'updatedBy' | 'unknown'
type PropertyTypeEnum = 'text' | 'number' | 'select' | 'multiSelect' | 'date' | 'person' | 'multiPerson' | 'file' | 'checkbox' | 'url' | 'email' | 'phone' | 'createdTime' | 'createdBy' | 'updatedTime' | 'updatedBy' | 'unknown'
interface IPropertyOption {
id: string

View File

@ -528,6 +528,31 @@ exports[`components/cardDetail/CardDetailProperties should show property types m
class="noicon"
/>
</div>
<div
aria-label="Multi person"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Multi person
</div>
</div>
<div
class="noicon"
/>
</div>
<div
aria-label="Checkbox"
class="MenuOption TextOption menu-option"

View File

@ -289,6 +289,23 @@
}
}
.MultiPerson .react-select__value-container--is-multi {
display: block;
white-space: nowrap;
.react-select__multi-value {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 24px;
display: inline-flex;
color: rgb(var(--center-channel-color-rgb));
.MultiPerson-item,
.react-select__multi-value__label {
color: inherit;
}
}
}
@media screen and (max-width: 768px) {
margin-left: 0 !important;
}

View File

@ -13,6 +13,7 @@ import SelectProperty from './select/property'
import MultiSelectProperty from './multiselect/property'
import DateProperty from './date/property'
import PersonProperty from './person/property'
import MultiPersonProperty from './multiperson/property'
import CheckboxProperty from './checkbox/property'
import UnknownProperty from './unknown/property'
@ -52,6 +53,7 @@ registry.register(new SelectProperty())
registry.register(new MultiSelectProperty())
registry.register(new DateProperty())
registry.register(new PersonProperty())
registry.register(new MultiPersonProperty())
registry.register(new CheckboxProperty())
registry.register(new CreatedTimeProperty())
registry.register(new CreatedByProperty())

View File

@ -0,0 +1,433 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`properties/multiperson not readonly 1`] = `
<div>
<div
class="MultiPerson octo-propertyvalue css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="react-select__control css-18140j1-Control"
>
<div
class="react-select__value-container react-select__value-container--is-multi react-select__value-container--has-value css-o7cxt9-ValueContainer"
>
<div
class="css-1rhbuit-multiValue react-select__multi-value"
>
<div
class="css-12jo7m5 react-select__multi-value__label"
>
<div
class="MultiPerson-item"
>
username-1
</div>
</div>
<div
aria-label="Remove [object Object]"
class="css-xb97g8 react-select__multi-value__remove"
role="button"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="14"
viewBox="0 0 20 20"
width="14"
>
<path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/>
</svg>
</div>
</div>
<div
class="css-1rhbuit-multiValue react-select__multi-value"
>
<div
class="css-12jo7m5 react-select__multi-value__label"
>
<div
class="MultiPerson-item"
>
username-2
</div>
</div>
<div
aria-label="Remove [object Object]"
class="css-xb97g8 react-select__multi-value__remove"
role="button"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="14"
viewBox="0 0 20 20"
width="14"
>
<path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/>
</svg>
</div>
</div>
<div
class="react-select__input-container css-ox1y69-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="react-select__input"
id="react-select-3-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class="react-select__indicator react-select__clear-indicator css-tpaeio-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/>
</svg>
</div>
<span
class="react-select__indicator-separator css-43ykx9-indicatorSeparator"
/>
<div
aria-hidden="true"
class="react-select__indicator react-select__dropdown-indicator css-19sxey8-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
`;
exports[`properties/multiperson not readonly not existing user 1`] = `
<div>
<div
class="MultiPerson octo-propertyvalue css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="react-select__control css-18140j1-Control"
>
<div
class="react-select__value-container react-select__value-container--is-multi css-433wy7-ValueContainer"
>
<div
class="react-select__placeholder css-14el2xx-placeholder"
id="react-select-2-placeholder"
>
Empty
</div>
<div
class="react-select__input-container css-ox1y69-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-describedby="react-select-2-placeholder"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="react-select__input"
id="react-select-2-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="react-select__indicator-separator css-43ykx9-indicatorSeparator"
/>
<div
aria-hidden="true"
class="react-select__indicator react-select__dropdown-indicator css-19sxey8-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
`;
exports[`properties/multiperson readonly view 1`] = `
<div>
<div
class="MultiPerson octo-propertyvalue octo-propertyvalue--readonly"
>
<div
class="MultiPerson-item"
>
username-1
</div>
<div
class="MultiPerson-item"
>
username-2
</div>
</div>
</div>
`;
exports[`properties/multiperson user dropdown open 1`] = `
<div>
<div
class="MultiPerson octo-propertyvalue css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-4-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
>
<span
id="aria-selection"
/>
<span
id="aria-context"
>
option username-3 focused, 3 of 3. 1 result available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
</span>
</span>
<div
class="react-select__control react-select__control--is-focused react-select__control--menu-is-open css-18140j1-Control"
>
<div
class="react-select__value-container react-select__value-container--is-multi react-select__value-container--has-value css-o7cxt9-ValueContainer"
>
<div
class="css-1rhbuit-multiValue react-select__multi-value"
>
<div
class="css-12jo7m5 react-select__multi-value__label"
>
<div
class="MultiPerson-item"
>
username-1
</div>
</div>
<div
aria-label="Remove [object Object]"
class="css-xb97g8 react-select__multi-value__remove"
role="button"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="14"
viewBox="0 0 20 20"
width="14"
>
<path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/>
</svg>
</div>
</div>
<div
class="css-1rhbuit-multiValue react-select__multi-value"
>
<div
class="css-12jo7m5 react-select__multi-value__label"
>
<div
class="MultiPerson-item"
>
username-2
</div>
</div>
<div
aria-label="Remove [object Object]"
class="css-xb97g8 react-select__multi-value__remove"
role="button"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="14"
viewBox="0 0 20 20"
width="14"
>
<path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/>
</svg>
</div>
</div>
<div
class="react-select__input-container css-ox1y69-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-controls="react-select-4-listbox"
aria-expanded="true"
aria-haspopup="true"
aria-owns="react-select-4-listbox"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="react-select__input"
id="react-select-4-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class="react-select__indicator react-select__clear-indicator css-13eygzs-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/>
</svg>
</div>
<span
class="react-select__indicator-separator css-43ykx9-indicatorSeparator"
/>
<div
aria-hidden="true"
class="react-select__indicator react-select__dropdown-indicator css-hl9mox-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
<div
class="react-select__menu css-10b6da7-menu"
id="react-select-4-listbox"
>
<div
class="react-select__menu-list react-select__menu-list--is-multi css-g29tl0-MenuList"
>
<div
aria-disabled="false"
class="react-select__option react-select__option--is-focused css-1bwtvog-option"
id="react-select-4-option-2"
tabindex="-1"
>
<div
class="MultiPerson-item"
>
username-3
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,97 @@
.MultiPerson {
padding: 4px 8px;
margin-right: 20px;
min-width: 180px;
display: flex;
align-items: center;
border-radius: 4px;
&.readonly {
overflow: hidden;
text-overflow: ellipsis;
min-width: unset;
}
.MultiPerson-item {
display: flex;
align-items: center;
img {
border-radius: 50px;
width: 24px;
height: 24px;
margin-right: 6px;
}
}
.react-select__menu {
background: rgba(var(--center-channel-bg-rgb), 1);
box-shadow: var(--elevation-4);
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
border-radius: 4px;
}
.react-select__single-value {
margin: 0;
position: relative;
top: 0;
max-width: 100%;
}
.react-select__value-container--is-multi {
display: inline-flex;
.react-select__multi-value {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 24px;
display: inline-flex;
color: rgb(var(--center-channel-color-rgb));
.MultiPerson-item,
.react-select__multi-value__label {
color: inherit;
}
}
}
.react-select__multi-value__remove {
font-size: 18px;
color: rgba(var(--center-channel-color-rgb), 0.56);
margin: 6px;
border-radius: 100%;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.26);
}
}
.react-select__option {
display: flex;
align-items: center;
height: 40px;
padding: 0 40px 0 20px;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
}
&:active {
background: rgba(var(--button-bg-rgb), 0.08);
}
&.react-select__option--is-selected {
background: rgba(var(--button-bg-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 1);
}
.MultiPerson-item {
img {
margin-right: 12px;
}
}
}
.react-select__menu-list {
border: 0;
}
}

View File

@ -0,0 +1,185 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {Provider as ReduxProvider} from 'react-redux'
import {render, waitFor} from '@testing-library/react'
import configureStore from 'redux-mock-store'
import {act} from 'react-dom/test-utils'
import userEvent from '@testing-library/user-event'
import {wrapIntl} from '../../testUtils'
import {IPropertyTemplate, Board} from '../../blocks/board'
import {Card} from '../../blocks/card'
import MultiPersonProperty from './property'
import MultiPerson from './multiperson'
describe('properties/multiperson', ()=> {
const mockStore = configureStore([])
const state = {
users: {
boardUsers: {
'user-id-1': {
id: 'user-id-1',
username: 'username-1',
email: 'user-1@example.com',
props: {},
create_at: 1621315184,
update_at: 1621315184,
delete_at: 0,
},
'user-id-2': {
id: 'user-id-2',
username: 'username-2',
email: 'user-2@example.com',
props: {},
create_at: 1621315184,
update_at: 1621315184,
delete_at: 0,
},
'user-id-3': {
id: 'user-id-3',
username: 'username-3',
email: 'user-3@example.com',
props: {},
create_at: 1621315184,
update_at: 1621315184,
delete_at: 0,
},
},
},
clientConfig: {
value: {
teammateNameDisplay: 'username',
},
},
}
test('not readonly not existing user', async () => {
const store = mockStore(state)
const component = wrapIntl(
<ReduxProvider store={store}>
<MultiPerson
property={new MultiPersonProperty()}
propertyValue={['user-id-4']}
readOnly={false}
showEmptyPlaceholder={false}
propertyTemplate={{} as IPropertyTemplate}
board={{} as Board}
card={{} as Card}
/>
</ReduxProvider>,
)
const renderResult = render(component)
const container = await waitFor(() => {
if (!renderResult.container) {
return Promise.reject(new Error('container not found'))
}
return Promise.resolve(renderResult.container)
})
expect(container).toMatchSnapshot()
})
test('not readonly', async () => {
const store = mockStore(state)
const component = wrapIntl(
<ReduxProvider store={store}>
<MultiPerson
property={new MultiPersonProperty()}
propertyValue={['user-id-1', 'user-id-2']}
readOnly={false}
showEmptyPlaceholder={false}
propertyTemplate={{} as IPropertyTemplate}
board={{} as Board}
card={{} as Card}
/>
</ReduxProvider>,
)
const renderResult = render(component)
const container = await waitFor(() => {
if (!renderResult.container) {
return Promise.reject(new Error('container not found'))
}
return Promise.resolve(renderResult.container)
})
expect(container).toMatchSnapshot()
})
test('readonly view', async () => {
const store = mockStore(state)
const component = wrapIntl(
<ReduxProvider store={store}>
<MultiPerson
property={new MultiPersonProperty()}
propertyValue={['user-id-1', 'user-id-2']}
readOnly={true}
showEmptyPlaceholder={false}
propertyTemplate={{} as IPropertyTemplate}
board={{} as Board}
card={{} as Card}
/>
</ReduxProvider>,
)
const renderResult = render(component)
const container = await waitFor(() => {
if (!renderResult.container) {
return Promise.reject(new Error('container not found'))
}
return Promise.resolve(renderResult.container)
})
expect(container).toMatchSnapshot()
})
test('user dropdown open', async () => {
const store = mockStore(state)
const component = wrapIntl(
<ReduxProvider store={store}>
<MultiPerson
property={new MultiPersonProperty()}
propertyValue={['user-id-1', 'user-id-2']}
readOnly={false}
showEmptyPlaceholder={false}
propertyTemplate={{} as IPropertyTemplate}
board={{} as Board}
card={{} as Card}
/>
</ReduxProvider>,
)
const renderResult = render(component)
const container = await waitFor(() => {
if (!renderResult.container) {
return Promise.reject(new Error('container not found'))
}
return Promise.resolve(renderResult.container)
})
if (container) {
// this is the actual element where the click event triggers
// opening of the dropdown
const userProperty = container.querySelector('.MultiPerson > div > div:nth-child(1) > div:nth-child(3) > input')
expect(userProperty).not.toBeNull()
act(() => {
userEvent.click(userProperty as Element)
})
expect(container).toMatchSnapshot()
} else {
throw new Error('container should have been initialized')
}
})
})

View File

@ -0,0 +1,128 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react'
import Select from 'react-select'
import {CSSObject} from '@emotion/serialize'
import {getSelectBaseStyle} from '../../theme'
import {IUser} from '../../user'
import {Utils} from '../../utils'
import mutator from '../../mutator'
import {useAppSelector} from '../../store/hooks'
import { getBoardUsers, getBoardUsersList } from '../../store/users'
import { PropertyProps } from '../types'
import {ClientConfig} from '../../config/clientConfig'
import {getClientConfig} from '../../store/clientConfig'
import './multiperson.scss'
const imageURLForUser = (window as any).Components?.imageURLForUser
const selectStyles = {
...getSelectBaseStyle(),
option: (provided: CSSObject, state: {isFocused: boolean}): CSSObject => ({
...provided,
background: state.isFocused ? 'rgba(var(--center-channel-color-rgb), 0.1)' : 'rgb(var(--center-channel-bg-rgb))',
color: state.isFocused ? 'rgb(var(--center-channel-color-rgb))' : 'rgb(var(--center-channel-color-rgb))',
padding: '8px',
}),
control: (): CSSObject => ({
border: 0,
width: '100%',
margin: '0',
}),
valueContainer: (provided: CSSObject): CSSObject => ({
...provided,
padding: 'unset',
overflow: 'unset',
}),
singleValue: (provided: CSSObject): CSSObject => ({
...provided,
position: 'static',
top: 'unset',
transform: 'unset',
}),
menu: (provided: CSSObject): CSSObject => ({
...provided,
width: 'unset',
background: 'rgb(var(--center-channel-bg-rgb))',
minWidth: '260px',
}),
}
const MultiPerson = (props: PropertyProps) => {
const {card, board, propertyTemplate, propertyValue, readOnly} = props
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
const boardUsersById = useAppSelector<{[key:string]: IUser}>(getBoardUsers)
const boardUsers = useAppSelector<IUser[]>(getBoardUsersList)
const formatOptionLabel = (user: any) => {
let profileImg
if (imageURLForUser) {
profileImg = imageURLForUser(user.id)
}
return (
<div key={user.id} className='MultiPerson-item'>
{profileImg && (
<img
alt='MultiPerson-avatar'
src={profileImg}
/>
)}
{Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}
</div>
)
}
const onChange = useCallback((newValue) => mutator.changePropertyValue(board.id, card, propertyTemplate.id, newValue), [board.id, card, propertyTemplate.id])
let users: IUser[] = []
if(typeof propertyValue === 'string') {
users = [boardUsersById[propertyValue as string]]
} else if(Array.isArray(propertyValue)) {
users = propertyValue.map(id => boardUsersById[id])
}
if (readOnly) {
return (
<div className={`MultiPerson ${props.property.valueClassName(true)}`}>
{users ? users.map(user => formatOptionLabel(user)) : propertyValue}
</div>
)
}
return (
<Select
isMulti
options={boardUsers}
isSearchable={true}
isClearable={true}
placeholder={'Empty'}
className={`MultiPerson ${props.property.valueClassName(props.readOnly)}`}
classNamePrefix={'react-select'}
formatOptionLabel={formatOptionLabel}
styles={selectStyles}
getOptionLabel={(o: IUser) => o.username}
getOptionValue={(a: IUser) => a.id}
value={users}
onChange={(item, action)=> {
if (action.action === 'select-option') {
onChange(item.map(a => a.id) || [])
} else if (action.action === 'clear') {
onChange([])
} else if (action.action === 'remove-value') {
onChange(item.filter(a => a.id !== action.removedValue.id).map(b => b.id) || [])
}
}}
/>
)
}
export default MultiPerson

View File

@ -0,0 +1,12 @@
import {IntlShape} from 'react-intl'
import {PropertyType, PropertyTypeEnum} from '../types'
import MultiPerson from './multiperson'
export default class MultiPersonProperty extends PropertyType {
Editor = MultiPerson
name = 'MultiPerson'
type = 'multiPerson' as PropertyTypeEnum
displayName = (intl:IntlShape) => intl.formatMessage({id: 'PropertyType.MultiPerson', defaultMessage: 'Multi person'})
}

View File

@ -298,6 +298,21 @@ function sortCards(cards: Card[], lastCommentByCard: {[key: string]: CommentBloc
bValue = template.options.find((o) => o.id === (Array.isArray(bValue) ? bValue[0] : bValue))?.value || ''
}
if (template.type === 'multiPerson') {
aValue = Array.isArray(aValue) && aValue.length !== 0 && usersById !== {} ? aValue.map((id) => {
if(usersById[id] !== undefined)
return usersById[id].username
return ''
}).toString() : aValue
bValue = Array.isArray(bValue) && bValue.length !== 0 && usersById !== {} ? bValue.map((id) => {
if (usersById[id] !== undefined) {
return usersById[id].username
}
return ''
}).toString() : bValue
}
result = (aValue as string).localeCompare(bValue as string)
}

View File

@ -296,6 +296,31 @@ exports[`widgets/PropertyMenu should match snapshot 1`] = `
class="noicon"
/>
</div>
<div
aria-label="Multi person"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Multi person
</div>
</div>
<div
class="noicon"
/>
</div>
<div
aria-label="Checkbox"
class="MenuOption TextOption menu-option"