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:
parent
a1a67390fc
commit
ad3b8fd454
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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>
|
||||
`;
|
97
webapp/src/properties/multiperson/multiperson.scss
Normal file
97
webapp/src/properties/multiperson/multiperson.scss
Normal 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;
|
||||
}
|
||||
}
|
185
webapp/src/properties/multiperson/multiperson.test.tsx
Normal file
185
webapp/src/properties/multiperson/multiperson.test.tsx
Normal 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')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
})
|
128
webapp/src/properties/multiperson/multiperson.tsx
Normal file
128
webapp/src/properties/multiperson/multiperson.tsx
Normal 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
|
12
webapp/src/properties/multiperson/property.tsx
Normal file
12
webapp/src/properties/multiperson/property.tsx
Normal 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'})
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user