1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-03-06 15:36:17 +02:00

Board calculations (#1464)

* Added menu options to choose calculation

* Made calculation option component generic for use in kanban and table

* Added property type based calculation option menu

* WIP

* Prepared submenu

* Populated submenu

* WIP

* WIP

* Base implementation complete

* Done

* minor cleanup

* Updating UI for board calculations

# Conflicts:
#	webapp/src/components/kanban/calculation/calculation.tsx

* Updating UI for board

* Highlighted currently selected option

* Fixed existsing tests

* Fixed existsing tests

* Added tests

* Added tests

* Fixed some plugin CSS issues

* Fixed a unintentional snapshot update

* Fixed a test

* Fixed a test

* Fixed a test

* Fixed dashboard tests

* Fixed some review comments

* Updated snapshots for change in Button classname

* Fixed test after syncing with main

Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com>
This commit is contained in:
Harshil Sharma 2021-10-13 13:26:14 +05:30 committed by GitHub
parent 0434e7d6b6
commit c4ee743a10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1336 additions and 207 deletions

View File

@ -29,6 +29,7 @@ type BoardFields = {
showDescription?: boolean
isTemplate?: boolean
cardProperties: IPropertyTemplate[]
columnCalculations: Record<string, string>
}
type Board = Block & {

View File

@ -9,6 +9,11 @@ import {FilterGroup, createFilterGroup} from './filterGroup'
type IViewType = 'board' | 'table' | 'gallery' // | 'calendar' | 'list'
type ISortOption = { propertyId: '__title' | string, reversed: boolean }
type KanbanCalculationFields = {
calculation: string
propertyId: string
}
type BoardViewFields = {
viewType: IViewType
groupById?: string
@ -21,6 +26,7 @@ type BoardViewFields = {
cardOrder: string[]
columnWidths: Record<string, number>
columnCalculations: Record<string, string>
kanbanCalculations: Record<string, KanbanCalculationFields>
defaultTemplateId: string
}
@ -45,6 +51,7 @@ function createBoardView(block?: Block): BoardView {
cardOrder: block?.fields.cardOrder?.slice() || [],
columnWidths: {...(block?.fields.columnWidths || {})},
columnCalculations: {...(block?.fields.columnCalculations) || {}},
kanbanCalculations: {...(block?.fields.kanbanCalculations) || {}},
defaultTemplateId: block?.fields.defaultTemplateId || '',
},
}
@ -57,4 +64,4 @@ function sortBoardViewsAlphabetically(views: BoardView[]): BoardView[] {
}).sort((v1, v2) => v1.title.localeCompare(v2.title)).map((v) => v.view)
}
export {BoardView, IViewType, ISortOption, sortBoardViewsAlphabetically, createBoardView}
export {BoardView, IViewType, ISortOption, sortBoardViewsAlphabetically, createBoardView, KanbanCalculationFields}

View File

@ -45,7 +45,7 @@ exports[`components/propertyValueElement should match snapshot, date, array valu
class="DateRange empty octo-propertyvalue"
>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -95,13 +95,13 @@ exports[`components/calculations/Calculation should match snapshot - option chan
class="css-1f43avz-a11yText-A11yText"
/>
<div
class=" css-1s59geg-Control"
class="CalculationOptions__control css-1s59geg-Control"
>
<div
class=" css-1mxrbau-ValueContainer"
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
>
<div
class=" css-1brck82-singleValue"
class="CalculationOptions__single-value css-1brck82-singleValue"
>
Calculate
</div>
@ -115,11 +115,11 @@ exports[`components/calculations/Calculation should match snapshot - option chan
/>
</div>
<div
class=" css-1hb7zxy-IndicatorsContainer"
class="CalculationOptions__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class=" css-tpaeio-indicatorContainer"
class="CalculationOptions__indicator CalculationOptions__clear-indicator css-tpaeio-indicatorContainer"
>
<svg
aria-hidden="true"
@ -135,11 +135,11 @@ exports[`components/calculations/Calculation should match snapshot - option chan
</svg>
</div>
<span
class=" css-43ykx9-indicatorSeparator"
class="CalculationOptions__indicator-separator css-43ykx9-indicatorSeparator"
/>
<div
aria-hidden="true"
class=" css-wpsttr-indicatorContainer"
class="CalculationOptions__indicator CalculationOptions__dropdown-indicator css-wpsttr-indicatorContainer"
>
<i
class="CompassIcon icon-chevron-up ChevronUpIcon"

View File

@ -21,13 +21,13 @@ exports[`components/calculations/Options should match snapshot 1`] = `
</span>
</span>
<div
class=" css-1s59geg-Control"
class="CalculationOptions__control CalculationOptions__control--is-focused css-1s59geg-Control"
>
<div
class=" css-1mxrbau-ValueContainer"
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
>
<div
class=" css-1brck82-singleValue"
class="CalculationOptions__single-value css-1brck82-singleValue"
>
Calculate
</div>
@ -41,11 +41,11 @@ exports[`components/calculations/Options should match snapshot 1`] = `
/>
</div>
<div
class=" css-1hb7zxy-IndicatorsContainer"
class="CalculationOptions__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class=" css-13eygzs-indicatorContainer"
class="CalculationOptions__indicator CalculationOptions__clear-indicator css-13eygzs-indicatorContainer"
>
<svg
aria-hidden="true"
@ -61,11 +61,11 @@ exports[`components/calculations/Options should match snapshot 1`] = `
</svg>
</div>
<span
class=" css-43ykx9-indicatorSeparator"
class="CalculationOptions__indicator-separator css-43ykx9-indicatorSeparator"
/>
<div
aria-hidden="true"
class=" css-1f9iddu-indicatorContainer"
class="CalculationOptions__indicator CalculationOptions__dropdown-indicator css-1f9iddu-indicatorContainer"
>
<i
class="CompassIcon icon-chevron-up ChevronUpIcon"
@ -99,17 +99,17 @@ exports[`components/calculations/Options should match snapshot menu open 1`] = `
<span
id="aria-context"
>
10 results 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.
2 results 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=" css-1s59geg-Control"
class="CalculationOptions__control CalculationOptions__control--is-focused CalculationOptions__control--menu-is-open css-1s59geg-Control"
>
<div
class=" css-1mxrbau-ValueContainer"
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
>
<div
class=" css-1brck82-singleValue"
class="CalculationOptions__single-value css-1brck82-singleValue"
>
Calculate
</div>
@ -123,11 +123,11 @@ exports[`components/calculations/Options should match snapshot menu open 1`] = `
/>
</div>
<div
class=" css-1hb7zxy-IndicatorsContainer"
class="CalculationOptions__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class=" css-13eygzs-indicatorContainer"
class="CalculationOptions__indicator CalculationOptions__clear-indicator css-13eygzs-indicatorContainer"
>
<svg
aria-hidden="true"
@ -143,11 +143,11 @@ exports[`components/calculations/Options should match snapshot menu open 1`] = `
</svg>
</div>
<span
class=" css-43ykx9-indicatorSeparator"
class="CalculationOptions__indicator-separator css-43ykx9-indicatorSeparator"
/>
<div
aria-hidden="true"
class=" css-1f9iddu-indicatorContainer"
class="CalculationOptions__indicator CalculationOptions__dropdown-indicator css-1f9iddu-indicatorContainer"
>
<i
class="CompassIcon icon-chevron-up ChevronUpIcon"
@ -156,81 +156,25 @@ exports[`components/calculations/Options should match snapshot menu open 1`] = `
</div>
</div>
<div
class=" css-1kslk4z-menu"
class="CalculationOptions__menu css-1rsmi4x-menu"
>
<div
class=" css-g29tl0-MenuList"
class="CalculationOptions__menu-list css-g29tl0-MenuList"
>
<div
class=" css-1cnkr6z-option"
class="CalculationOptions__option css-14xsrqy-option"
id="react-select-3-option-0"
tabindex="-1"
>
None
</div>
<div
class=" css-14xsrqy-option"
id="react-select-3-option-1"
tabindex="-1"
>
Count
</div>
<div
class=" css-14xsrqy-option"
id="react-select-3-option-2"
tabindex="-1"
>
Count Value
</div>
<div
class=" css-14xsrqy-option"
id="react-select-3-option-3"
tabindex="-1"
>
Count Unique Values
</div>
<div
class=" css-14xsrqy-option"
id="react-select-3-option-4"
tabindex="-1"
>
Sum
</div>
<div
class=" css-14xsrqy-option"
id="react-select-3-option-5"
tabindex="-1"
>
Average
</div>
<div
class=" css-14xsrqy-option"
id="react-select-3-option-6"
tabindex="-1"
>
Median
</div>
<div
class=" css-14xsrqy-option"
id="react-select-3-option-7"
tabindex="-1"
>
Min
</div>
<div
class=" css-14xsrqy-option"
id="react-select-3-option-8"
class="CalculationOptions__option css-14xsrqy-option"
id="react-select-3-option-1"
tabindex="-1"
>
Max
</div>
<div
class=" css-14xsrqy-option"
id="react-select-3-option-9"
tabindex="-1"
>
Range
</div>
</div>
</div>
<input

View File

@ -9,6 +9,8 @@ import userEvent from '@testing-library/user-event'
import {TestBlockFactory} from '../../test/testBlockFactory'
import {wrapIntl} from '../../testUtils'
import {TableCalculationOptions} from '../table/calculation/tableCalculationOptions'
import Calculation from './calculation'
describe('components/calculations/Calculation', () => {
@ -42,6 +44,7 @@ describe('components/calculations/Calculation', () => {
type: 'text',
options: [],
}}
optionsComponent={TableCalculationOptions}
/>,
)
@ -67,6 +70,7 @@ describe('components/calculations/Calculation', () => {
type: 'text',
options: [],
}}
optionsComponent={TableCalculationOptions}
/>,
)
@ -92,6 +96,7 @@ describe('components/calculations/Calculation', () => {
type: 'text',
options: [],
}}
optionsComponent={TableCalculationOptions}
/>,
)
@ -117,6 +122,7 @@ describe('components/calculations/Calculation', () => {
type: 'text',
options: [],
}}
optionsComponent={TableCalculationOptions}
/>,
)
@ -146,6 +152,7 @@ describe('components/calculations/Calculation', () => {
type: 'text',
options: [],
}}
optionsComponent={TableCalculationOptions}
/>,
)

View File

@ -9,7 +9,7 @@ import {IPropertyTemplate} from '../../blocks/board'
import ChevronUp from '../../widgets/icons/chevronUp'
import {CalculationOptions, Options} from './options'
import {CommonCalculationOptionProps, Options} from './options'
import Calculations from './calculations'
import './calculation.scss'
@ -25,6 +25,7 @@ type Props = {
cards: readonly Card[]
property: IPropertyTemplate
hovered: boolean
optionsComponent: React.ComponentType<CommonCalculationOptionProps>
}
const Calculation = (props: Props): JSX.Element => {
@ -32,6 +33,16 @@ const Calculation = (props: Props): JSX.Element => {
const valueOption = Options[value]
const intl = useIntl()
const option = (
<props.optionsComponent
value={value}
menuOpen={props.menuOpen}
onClose={props.onMenuClose}
onChange={props.onChange}
property={props.property}
/>
)
return (
// tabindex is needed to make onBlur work on div.
@ -46,14 +57,8 @@ const Calculation = (props: Props): JSX.Element => {
>
{
props.menuOpen && (
<div >
<CalculationOptions
value={value}
menuOpen={props.menuOpen}
onClose={props.onMenuClose}
onChange={props.onChange}
property={props.property}
/>
<div>
{option}
</div>
)
}

View File

@ -19,6 +19,12 @@ describe('components/calculations/Options', () => {
value={'none'}
onChange={() => {}}
property={property}
menuOpen={false}
options={[{
label: 'Count',
value: 'count',
displayName: 'Count',
}]}
/>
)
@ -37,6 +43,18 @@ describe('components/calculations/Options', () => {
menuOpen={true}
onChange={() => {}}
property={property}
options={[
{
label: 'Count',
value: 'count',
displayName: 'Count',
},
{
label: 'Max',
value: 'max',
displayName: 'Max',
},
]}
/>
)

View File

@ -16,7 +16,7 @@ type Option = {
displayName: string
}
const Options:Record<string, Option> = {
export const Options:Record<string, Option> = {
none: {value: 'none', label: 'None', displayName: 'Calculate'},
count: {value: 'count', label: 'Count', displayName: 'Count'},
countValue: {value: 'countValue', label: 'Count Value', displayName: 'Values'},
@ -36,7 +36,7 @@ const Options:Record<string, Option> = {
dateRange: {value: 'dateRange', label: 'Range', displayName: 'Range'},
}
const optionsByType: Map<string, Option[]> = new Map([
export const optionsByType: Map<string, Option[]> = new Map([
['common', [Options.none, Options.count, Options.countValue, Options.countUniqueValue]],
['checkbox', [Options.countChecked, Options.countUnchecked, Options.percentChecked, Options.percentUnchecked]],
['number', [Options.sum, Options.average, Options.median, Options.min, Options.max, Options.range]],
@ -45,6 +45,22 @@ const optionsByType: Map<string, Option[]> = new Map([
['updatedTime', [Options.earliest, Options.latest, Options.dateRange]],
])
export const typesByOptions: Map<string, string[]> = generateTypesByOption()
function generateTypesByOption(): Map<string, string[]> {
const mapping = new Map<string, string[]>()
optionsByType.forEach((options, type) => {
options.forEach((option) => {
const types = mapping.get(option.value) || []
types.push(type)
mapping.set(option.value, types)
})
})
return mapping
}
const baseStyles = getSelectBaseStyle()
const styles = {
@ -65,7 +81,7 @@ const styles = {
minWidth: '100%',
width: 'max-content',
background: 'rgb(var(--center-channel-bg-rgb))',
right: '0',
left: '0',
marginBottom: '0',
}),
singleValue: (provided: CSSObject): CSSObject => ({
@ -90,20 +106,22 @@ const DropdownIndicator = (props: IndicatorProps<Option, false>) => {
)
}
type Props = {
// Calculation option props shared by all implementations of calculation options
type CommonCalculationOptionProps = {
value: string,
menuOpen?: boolean
menuOpen: boolean
onClose?: () => void
onChange: (value: string) => void
property: IPropertyTemplate
components?: {[key:string]: (props: any) => JSX.Element}
onChange: (data: any) => void
property?: IPropertyTemplate
}
const CalculationOptions = (props: Props): JSX.Element => {
const options = [...optionsByType.get('common')!]
if (optionsByType.get(props.property.type)) {
options.push(...optionsByType.get(props.property.type)!)
}
// Props used by the base calculation option component
type BaseCalculationOptionProps = CommonCalculationOptionProps & {
options: Option[]
}
const CalculationOptions = (props: BaseCalculationOptionProps): JSX.Element => {
return (
<Select
styles={styles}
@ -112,10 +130,11 @@ const CalculationOptions = (props: Props): JSX.Element => {
isClearable={true}
name={'calculation_options'}
className={'CalculationOptions'}
options={options}
classNamePrefix={'CalculationOptions'}
options={props.options}
menuPlacement={'auto'}
isSearchable={false}
components={{DropdownIndicator}}
components={{DropdownIndicator, ...(props.components || {})}}
defaultMenuIsOpen={props.menuOpen}
autoFocus={true}
formatOptionLabel={(option: Option, meta) => {
@ -137,6 +156,6 @@ const CalculationOptions = (props: Props): JSX.Element => {
export {
CalculationOptions,
Options,
Option,
CommonCalculationOptionProps,
}

View File

@ -11,7 +11,7 @@ exports[`components/cardDetail/cardDetailContentsMenu return cardDetailContentsM
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -162,7 +162,7 @@ exports[`components/cardDetail/cardDetailContentsMenu return cardDetailContentsM
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -313,7 +313,7 @@ exports[`components/cardDetail/cardDetailContentsMenu return cardDetailContentsM
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -17,7 +17,7 @@ exports[`components/cardDetail/CardDetailProperties should match snapshot 1`] =
class="octo-propertyname"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -46,7 +46,7 @@ exports[`components/cardDetail/CardDetailProperties should match snapshot 1`] =
class="octo-propertyname add-property"
>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -0,0 +1,167 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/kanban/calculation/KanbanCalculation base case 1`] = `
<div>
<div
class="KanbanCalculation"
>
<button
class="Button"
type="button"
>
<span>
3
</span>
</button>
</div>
</div>
`;
exports[`components/kanban/calculation/KanbanCalculation calculations menu open 1`] = `
<div>
<div
class="KanbanCalculation"
>
<button
class="Button"
type="button"
>
<span>
3
</span>
</button>
<div
class="CalculationOptions css-2b097c-container"
>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
>
<span
id="aria-selection"
/>
<span
id="aria-context"
>
3 results 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="CalculationOptions__control CalculationOptions__control--is-focused CalculationOptions__control--menu-is-open css-1s59geg-Control"
>
<div
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
>
<div
class="CalculationOptions__single-value css-1brck82-singleValue"
>
Count
</div>
<input
aria-autocomplete="list"
class="css-wmatm6-dummyInput-DummyInput"
id="react-select-2-input"
readonly=""
tabindex="0"
value=""
/>
</div>
<div
class="CalculationOptions__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class="CalculationOptions__indicator CalculationOptions__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="CalculationOptions__indicator-separator css-43ykx9-indicatorSeparator"
/>
<div
aria-hidden="true"
class="CalculationOptions__indicator CalculationOptions__dropdown-indicator css-1f9iddu-indicatorContainer"
>
<i
class="CompassIcon icon-chevron-up ChevronUpIcon"
/>
</div>
</div>
</div>
<div
class="CalculationOptions__menu css-1rsmi4x-menu"
>
<div
class="CalculationOptions__menu-list css-g29tl0-MenuList"
>
<div
class="KanbanCalculationOptions_CustomOption active"
>
<span>
Count
</span>
</div>
<div
class="KanbanCalculationOptions_CustomOption "
>
<span>
Count Value
<i
class="CompassIcon icon-chevron-right ChevronRightIcon"
/>
</span>
</div>
<div
class="KanbanCalculationOptions_CustomOption "
>
<span>
Count Unique Values
<i
class="CompassIcon icon-chevron-right ChevronRightIcon"
/>
</span>
</div>
</div>
</div>
<input
name="calculation_options"
type="hidden"
value="count"
/>
</div>
</div>
</div>
`;
exports[`components/kanban/calculation/KanbanCalculation no menu should appear in readonly mode 1`] = `
<div>
<div
class="KanbanCalculation"
>
<button
class="Button"
type="button"
>
<span>
3
</span>
</button>
</div>
</div>
`;

View File

@ -0,0 +1,355 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/kanban/calculations/KanbanCalculationOptions base case 1`] = `
<div>
<div
class="CalculationOptions css-2b097c-container"
>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
>
<span
id="aria-selection"
/>
<span
id="aria-context"
>
Select is focused , press Down to open the menu,
</span>
</span>
<div
class="CalculationOptions__control CalculationOptions__control--is-focused css-1s59geg-Control"
>
<div
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
>
<div
class="CalculationOptions__single-value css-1brck82-singleValue"
>
Count
</div>
<input
aria-autocomplete="list"
class="css-wmatm6-dummyInput-DummyInput"
id="react-select-2-input"
readonly=""
tabindex="0"
value=""
/>
</div>
<div
class="CalculationOptions__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class="CalculationOptions__indicator CalculationOptions__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="CalculationOptions__indicator-separator css-43ykx9-indicatorSeparator"
/>
<div
aria-hidden="true"
class="CalculationOptions__indicator CalculationOptions__dropdown-indicator css-1f9iddu-indicatorContainer"
>
<i
class="CompassIcon icon-chevron-up ChevronUpIcon"
/>
</div>
</div>
</div>
<input
name="calculation_options"
type="hidden"
value="count"
/>
</div>
</div>
`;
exports[`components/kanban/calculations/KanbanCalculationOptions with menu open 1`] = `
<div>
<div
class="CalculationOptions css-2b097c-container"
>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
>
<span
id="aria-selection"
/>
<span
id="aria-context"
>
3 results 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="CalculationOptions__control CalculationOptions__control--is-focused CalculationOptions__control--menu-is-open css-1s59geg-Control"
>
<div
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
>
<div
class="CalculationOptions__single-value css-1brck82-singleValue"
>
Count
</div>
<input
aria-autocomplete="list"
class="css-wmatm6-dummyInput-DummyInput"
id="react-select-3-input"
readonly=""
tabindex="0"
value=""
/>
</div>
<div
class="CalculationOptions__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class="CalculationOptions__indicator CalculationOptions__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="CalculationOptions__indicator-separator css-43ykx9-indicatorSeparator"
/>
<div
aria-hidden="true"
class="CalculationOptions__indicator CalculationOptions__dropdown-indicator css-1f9iddu-indicatorContainer"
>
<i
class="CompassIcon icon-chevron-up ChevronUpIcon"
/>
</div>
</div>
</div>
<div
class="CalculationOptions__menu css-1rsmi4x-menu"
>
<div
class="CalculationOptions__menu-list css-g29tl0-MenuList"
>
<div
class="KanbanCalculationOptions_CustomOption active"
>
<span>
Count
</span>
</div>
<div
class="KanbanCalculationOptions_CustomOption "
>
<span>
Count Value
<i
class="CompassIcon icon-chevron-right ChevronRightIcon"
/>
</span>
</div>
<div
class="KanbanCalculationOptions_CustomOption "
>
<span>
Count Unique Values
<i
class="CompassIcon icon-chevron-right ChevronRightIcon"
/>
</span>
</div>
</div>
</div>
<input
name="calculation_options"
type="hidden"
value="count"
/>
</div>
</div>
`;
exports[`components/kanban/calculations/KanbanCalculationOptions with submenu open 1`] = `
<div>
<div
class="CalculationOptions css-2b097c-container"
>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
>
<span
id="aria-selection"
/>
<span
id="aria-context"
>
3 results 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="CalculationOptions__control CalculationOptions__control--is-focused CalculationOptions__control--menu-is-open css-1s59geg-Control"
>
<div
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
>
<div
class="CalculationOptions__single-value css-1brck82-singleValue"
>
Count
</div>
<input
aria-autocomplete="list"
class="css-wmatm6-dummyInput-DummyInput"
id="react-select-4-input"
readonly=""
tabindex="0"
value=""
/>
</div>
<div
class="CalculationOptions__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class="CalculationOptions__indicator CalculationOptions__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="CalculationOptions__indicator-separator css-43ykx9-indicatorSeparator"
/>
<div
aria-hidden="true"
class="CalculationOptions__indicator CalculationOptions__dropdown-indicator css-1f9iddu-indicatorContainer"
>
<i
class="CompassIcon icon-chevron-up ChevronUpIcon"
/>
</div>
</div>
</div>
<div
class="CalculationOptions__menu css-1rsmi4x-menu"
>
<div
class="CalculationOptions__menu-list css-g29tl0-MenuList"
>
<div
class="KanbanCalculationOptions_CustomOption active"
>
<span>
Count
</span>
</div>
<div
class="KanbanCalculationOptions_CustomOption "
>
<span>
Count Value
<i
class="CompassIcon icon-chevron-right ChevronRightIcon"
/>
</span>
</div>
<div
class="KanbanCalculationOptions_CustomOption "
>
<span>
Count Unique Values
<i
class="CompassIcon icon-chevron-right ChevronRightIcon"
/>
</span>
<div
class="dropdown-submenu"
>
<div
class="drops "
>
<span>
Status
</span>
</div>
<div
class="drops active"
>
<span>
Property 1
</span>
</div>
<div
class="drops "
>
<span>
Property 2
</span>
</div>
<div
class="drops "
>
<span>
Property 3
</span>
</div>
</div>
</div>
</div>
</div>
<input
name="calculation_options"
type="hidden"
value="count"
/>
</div>
</div>
`;

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/kanban/calculations/Option base case 1`] = `
<div>
<div
class="KanbanCalculationOptions_CustomOption "
>
<span>
Count Unique Values
<i
class="CompassIcon icon-chevron-right ChevronRightIcon"
/>
</span>
</div>
</div>
`;

View File

@ -0,0 +1,14 @@
.KanbanCalculation {
position: relative;
button {
cursor: pointer !important;
height: 24px;
padding: 0 6px;
min-width: 24px;
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.1);
}
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {render} from '@testing-library/react'
import {TestBlockFactory} from '../../../test/testBlockFactory'
import {wrapIntl} from '../../../testUtils'
import {KanbanCalculation} from './calculation'
describe('components/kanban/calculation/KanbanCalculation', () => {
const board = TestBlockFactory.createBoard()
const cards = [
TestBlockFactory.createCard(board),
TestBlockFactory.createCard(board),
TestBlockFactory.createCard(board),
]
test('base case', () => {
const component = wrapIntl((
<KanbanCalculation
cards={cards}
cardProperties={board.fields.cardProperties}
menuOpen={false}
onMenuClose={() => {}}
onMenuOpen={() => {}}
onChange={() => {}}
value={'count'}
property={board.fields.cardProperties[0]}
readonly={false}
/>
))
const {container} = render(component)
expect(container).toMatchSnapshot()
})
test('calculations menu open', () => {
const component = wrapIntl((
<KanbanCalculation
cards={cards}
cardProperties={board.fields.cardProperties}
menuOpen={true}
onMenuClose={() => {}}
onMenuOpen={() => {}}
onChange={() => {}}
value={'count'}
property={board.fields.cardProperties[0]}
readonly={false}
/>
))
const {container} = render(component)
expect(container).toMatchSnapshot()
})
test('no menu should appear in readonly mode', () => {
const component = wrapIntl((
<KanbanCalculation
cards={cards}
cardProperties={board.fields.cardProperties}
menuOpen={true}
onMenuClose={() => {}}
onMenuOpen={() => {}}
onChange={() => {}}
value={'count'}
property={board.fields.cardProperties[0]}
readonly={true}
/>
))
const {container} = render(component)
expect(container).toMatchSnapshot()
})
})

View File

@ -0,0 +1,59 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {useIntl} from 'react-intl'
import {Card} from '../../../blocks/card'
import Button from '../../../widgets/buttons/button'
import './calculation.scss'
import {IPropertyTemplate} from '../../../blocks/board'
import Calculations from '../../calculations/calculations'
import {KanbanCalculationOptions} from './calculationOptions'
type Props = {
cards: Card[]
cardProperties: IPropertyTemplate[]
menuOpen: boolean
onMenuClose: () => void
onMenuOpen: () => void
onChange: (data: { calculation: string, propertyId: string }) => void
value: string
property: IPropertyTemplate
readonly: boolean
}
function KanbanCalculation(props: Props): JSX.Element {
const intl = useIntl()
return (
<div className='KanbanCalculation'>
<Button
onClick={() => (props.menuOpen ? props.onMenuClose : props.onMenuOpen)()}
onBlur={props.onMenuClose}
>
{Calculations[props.value] ? Calculations[props.value](props.cards, props.property, intl) : ''}
</Button>
{
!props.readonly && props.menuOpen && (
<KanbanCalculationOptions
value={props.value}
property={props.property}
menuOpen={props.menuOpen}
onChange={(data: { calculation: string, propertyId: string }) => {
props.onChange(data)
props.onMenuClose()
}}
cardProperties={props.cardProperties}
/>
)
}
</div>
)
}
export {
KanbanCalculation,
}

View File

@ -0,0 +1,63 @@
.KanbanCalculationOptions_CustomOption {
color: rgba(var(--center-channel-color-rgb), 1);
min-height: 24px;
max-width: 220px;
width: 200px;
padding: 2px 12px;
cursor: pointer;
position: relative;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.1);
}
span {
overflow: hidden;
}
.drops {
padding: 2px 14px;
width: 200px;
&.active,
&.active:hover {
background-color: rgb(var(--sidebar-bg-rgb));
color: rgb(var(--sidebar-text-rgb));
}
}
.customs:hover,
.drops:hover {
background: rgba(var(--center-channel-color-rgb), 0.1);
}
.dropdown-submenu {
position: fixed;
overflow: auto;
background: rgb(var(--center-channel-bg-rgb));
border: 0;
box-shadow: 0 0 0 1px hsla(0, 0%, 0%, 0.1), 0 4px 11px hsla(0, 0%, 0%, 0.1);
z-index: 999;
padding: 6px 0;
border-radius: 4px;
}
.CompassIcon.icon-chevron-right {
float: right;
}
&.active {
background-color: rgb(var(--sidebar-bg-rgb));
> span {
color: rgb(var(--sidebar-text-rgb));
}
}
}
.CalculationOptions__menu {
.CalculationOptions__menu-list {
max-height: 400px;
padding: 8px 0;
}
}

View File

@ -0,0 +1,63 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {render} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {TestBlockFactory} from '../../../test/testBlockFactory'
import {KanbanCalculationOptions} from './calculationOptions'
describe('components/kanban/calculations/KanbanCalculationOptions', () => {
const board = TestBlockFactory.createBoard()
test('base case', () => {
const component = (
<KanbanCalculationOptions
value={'count'}
property={board.fields.cardProperties[1]}
menuOpen={false}
onChange={() => {}}
cardProperties={board.fields.cardProperties}
/>
)
const {container} = render(component)
expect(container).toMatchSnapshot()
})
test('with menu open', () => {
const component = (
<KanbanCalculationOptions
value={'count'}
property={board.fields.cardProperties[1]}
menuOpen={true}
onChange={() => {}}
cardProperties={board.fields.cardProperties}
/>
)
const {container} = render(component)
expect(container).toMatchSnapshot()
})
test('with submenu open', () => {
const component = (
<KanbanCalculationOptions
value={'count'}
property={board.fields.cardProperties[1]}
menuOpen={true}
onChange={() => {}}
cardProperties={board.fields.cardProperties}
/>
)
const {container, getByText} = render(component)
const countUniqueValuesOption = getByText('Count Unique Values')
expect(countUniqueValuesOption).toBeDefined()
userEvent.hover(countUniqueValuesOption)
expect(container).toMatchSnapshot()
})
})

View File

@ -0,0 +1,62 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {
CalculationOptions,
CommonCalculationOptionProps,
optionsByType,
} from '../../calculations/options'
import {IPropertyTemplate} from '../../../blocks/board'
import './calculationOption.scss'
import {Option, OptionProps} from './kanbanOption'
type Props = CommonCalculationOptionProps & {
cardProperties: IPropertyTemplate[]
onChange: (data: {calculation: string, propertyId: string}) => void
}
export const KanbanCalculationOptions = (props: Props): JSX.Element => {
const options: OptionProps[] = []
// Show common options, first,
// followed by type-specific functions
optionsByType.get('common')!.forEach((typeOption) => {
if (typeOption.value !== 'none') {
options.push({
...typeOption,
cardProperties: props.cardProperties,
onChange: props.onChange,
activeValue: props.value,
activeProperty: props.property!,
})
}
})
props.cardProperties.
map((property) => optionsByType.get(property.type) || []).
forEach((typeOptions) => {
typeOptions.forEach((typeOption) => {
options.push({
...typeOption,
cardProperties: props.cardProperties,
onChange: props.onChange,
activeValue: props.value,
activeProperty: props.property!,
})
})
})
return (
<CalculationOptions
value={props.value}
menuOpen={props.menuOpen}
onClose={props.onClose}
onChange={props.onChange}
property={props.property}
options={options}
components={{Option}}
/>
)
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {render} from '@testing-library/react'
import {TestBlockFactory} from '../../../test/testBlockFactory'
import {Option} from './kanbanOption'
describe('components/kanban/calculations/Option', () => {
const board = TestBlockFactory.createBoard()
test('base case', () => {
const component = (
<Option
data={{
label: 'Count Unique Values',
displayName: 'Unique',
value: 'countUniqueValue',
cardProperties: board.fields.cardProperties,
onChange: () => {},
activeValue: 'count',
activeProperty: board.fields.cardProperties[1],
}}
/>
)
const {container} = render(component)
expect(container).toMatchSnapshot()
})
})

View File

@ -0,0 +1,102 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react'
import {Option as SelectOption, typesByOptions} from '../../calculations/options'
import {IPropertyTemplate} from '../../../blocks/board'
import ChevronRight from '../../../widgets/icons/chevronRight'
import {Constants} from '../../../constants'
type OptionProps = SelectOption & {
cardProperties: IPropertyTemplate[]
onChange: (data: {calculation: string, propertyId: string}) => void
activeValue: string
activeProperty: IPropertyTemplate
}
const Option = (props: {data: OptionProps}): JSX.Element => {
const [submenu, setSubmenu] = useState(false)
const [height, setHeight] = useState(0)
const [menuOptionRight, setMenuOptionRight] = useState(0)
const [calculationToProperties, setCalculationToProperties] = useState<Map<string, IPropertyTemplate[]>>(new Map())
const toggleOption = (e: any) => {
if (submenu) {
setSubmenu(false)
} else {
const rect = e.target.getBoundingClientRect()
setHeight(rect.y)
setMenuOptionRight(rect.x + rect.width)
setSubmenu(true)
}
}
if (!calculationToProperties.get(props.data.value)) {
const supportedPropertyTypes = new Map<string, boolean>([])
if (typesByOptions.get(props.data.value)) {
(typesByOptions.get(props.data.value) || []).
forEach((propertyType) => supportedPropertyTypes.set(propertyType, true))
}
const supportedProperties = props.data.cardProperties.
filter((property) => supportedPropertyTypes.get(property.type) || supportedPropertyTypes.get('common'))
calculationToProperties.set(props.data.value, supportedProperties)
setCalculationToProperties(calculationToProperties)
}
return (
<div
className={`KanbanCalculationOptions_CustomOption ${props.data.activeValue === props.data.value ? 'active' : ''}`}
onMouseEnter={toggleOption}
onMouseLeave={toggleOption}
onClick={() => {
if (props.data.value !== 'count') {
return
}
props.data.onChange({
calculation: 'count',
propertyId: Constants.titleColumnId,
})
}}
>
<span>
{props.data.label} {props.data.value !== 'count' && <ChevronRight/>}
</span>
{
submenu && props.data.value !== 'count' && (
<div
className='dropdown-submenu'
style={{top: `${height - 10}px`, left: `${menuOptionRight}px`}}
>
{
calculationToProperties.get(props.data.value) &&
calculationToProperties.get(props.data.value)!.map((property) => (
<div
key={property.id}
className={`drops ${props.data.activeProperty.id === property.id ? 'active' : ''}`}
onClick={() => {
props.data.onChange({
calculation: props.data.value,
propertyId: property.id,
})
}}
>
<span>{property.name}</span>
</div>
))
}
</div>
)
}
</div>
)
}
export {
Option,
OptionProps,
}

View File

@ -29,8 +29,6 @@
}
> div {
margin-right: 8px;
&:last-child {
margin: 0;
}
@ -47,6 +45,7 @@
.Label {
max-width: 165px;
margin-right: 5px;
margin-bottom: 0;
.Editable {
background: transparent;

View File

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import React, {useCallback} from 'react'
import React, {useCallback, useState} from 'react'
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
import {Board, IPropertyOption, IPropertyTemplate, BoardGroup} from '../../blocks/board'
@ -166,6 +166,13 @@ const Kanban = (props: Props) => {
})
}, [cards, activeView, groupByProperty, props.selectedCardIds])
const [showCalculationsMenu, setShowCalculationsMenu] = useState<Map<string, boolean>>(new Map<string, boolean>())
const toggleOptions = (templateId: string, show: boolean) => {
const newShowOptions = new Map<string, boolean>(showCalculationsMenu)
newShowOptions.set(templateId, show)
setShowCalculationsMenu(newShowOptions)
}
return (
<div className='Kanban'>
<div
@ -186,6 +193,9 @@ const Kanban = (props: Props) => {
readonly={props.readonly}
propertyNameChanged={propertyNameChanged}
onDropToColumn={onDropToColumn}
calculationMenuOpen={showCalculationsMenu.get(group.option.id) || false}
onCalculationMenuOpen={() => toggleOptions(group.option.id, true)}
onCalculationMenuClose={() => toggleOptions(group.option.id, false)}
/>
))}

View File

@ -10,7 +10,6 @@ import {IPropertyOption, IPropertyTemplate, Board, BoardGroup} from '../../block
import {BoardView} from '../../blocks/boardView'
import {Card} from '../../blocks/card'
import mutator from '../../mutator'
import Button from '../../widgets/buttons/button'
import IconButton from '../../widgets/buttons/iconButton'
import AddIcon from '../../widgets/icons/add'
import DeleteIcon from '../../widgets/icons/delete'
@ -21,6 +20,8 @@ import MenuWrapper from '../../widgets/menuWrapper'
import Editable from '../../widgets/editable'
import Label from '../../widgets/label'
import {KanbanCalculation} from './calculation/calculation'
type Props = {
board: Board
activeView: BoardView
@ -31,8 +32,16 @@ type Props = {
addCard: (groupByOptionId?: string) => Promise<void>
propertyNameChanged: (option: IPropertyOption, text: string) => Promise<void>
onDropToColumn: (srcOption: IPropertyOption, card?: Card, dstOption?: IPropertyOption) => void
calculationMenuOpen: boolean
onCalculationMenuOpen: () => void
onCalculationMenuClose: () => void
}
const defaultCalculation = 'count'
const defaultProperty: IPropertyTemplate = {
id: Constants.titleColumnId,
} as IPropertyTemplate
export default function KanbanColumnHeader(props: Props): JSX.Element {
const {board, activeView, intl, group, groupByProperty} = props
const [groupTitle, setGroupTitle] = useState(group.option.value)
@ -66,6 +75,10 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
className += ' dragover'
}
const groupCalculation = props.activeView.fields.kanbanCalculations[props.group.option.id]
const calculationValue = groupCalculation ? groupCalculation.calculation : defaultCalculation
const calculationProperty = groupCalculation ? props.board.fields.cardProperties.find((property) => property.id === groupCalculation.propertyId) || defaultProperty : defaultProperty
return (
<div
key={group.option.id || 'empty'}
@ -108,7 +121,31 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
spellCheck={true}
/>
</Label>}
<Button>{`${group.cards.length}`}</Button>
<KanbanCalculation
cards={group.cards}
menuOpen={props.calculationMenuOpen}
value={calculationValue}
property={calculationProperty}
onMenuClose={props.onCalculationMenuClose}
onMenuOpen={props.onCalculationMenuOpen}
cardProperties={board.fields.cardProperties}
readonly={props.readonly}
onChange={(data: {calculation: string, propertyId: string}) => {
if (data.calculation === calculationValue && data.propertyId === calculationProperty.id) {
return
}
const newCalculations = {
...props.activeView.fields.kanbanCalculations,
}
newCalculations[props.group.option.id] = {
calculation: data.calculation,
propertyId: data.propertyId,
}
mutator.changeViewKanbanCalculations(props.activeView.id, props.activeView.fields.kanbanCalculations, newCalculations)
}}
/>
<div className='octo-spacer'/>
{!props.readonly &&
<>

View File

@ -6,7 +6,7 @@ exports[`components/properties/dateRange cancel set via text input 1`] = `
class="DateRange octo-propertyvalue"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -23,7 +23,7 @@ exports[`components/properties/dateRange handle clear 1`] = `
class="DateRange octo-propertyvalue"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -40,7 +40,7 @@ exports[`components/properties/dateRange returns default correctly 1`] = `
class="DateRange empty octo-propertyvalue"
>
<button
class="Button "
class="Button"
type="button"
>
<span />
@ -55,7 +55,7 @@ exports[`components/properties/dateRange returns local correctly - es local 1`]
class="DateRange octo-propertyvalue"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -72,7 +72,7 @@ exports[`components/properties/dateRange set via text input 1`] = `
class="DateRange octo-propertyvalue"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -89,7 +89,7 @@ exports[`components/properties/dateRange set via text input, es locale 1`] = `
class="DateRange octo-propertyvalue"
>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -165,7 +165,7 @@ exports[`components/table/Table extended should match snapshot with CreatedBy 1`
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -253,7 +253,7 @@ exports[`components/table/Table extended should match snapshot with CreatedBy 1`
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -553,7 +553,7 @@ exports[`components/table/Table extended should match snapshot with CreatedBy 2`
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -641,7 +641,7 @@ exports[`components/table/Table extended should match snapshot with CreatedBy 2`
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -941,7 +941,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedAt 1`
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -1029,7 +1029,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedAt 1`
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -1329,7 +1329,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedBy 1`
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -1417,7 +1417,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedBy 1`
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -1693,7 +1693,7 @@ exports[`components/table/Table should match snapshot 1`] = `
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -1950,7 +1950,7 @@ exports[`components/table/Table should match snapshot with GroupBy 1`] = `
</div>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -2168,7 +2168,7 @@ exports[`components/table/Table should match snapshot, read-only 1`] = `
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -39,7 +39,7 @@ exports[`should match snapshot on read only 1`] = `
</span>
</div>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -88,7 +88,7 @@ exports[`should match snapshot with Group 1`] = `
</span>
</div>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -159,7 +159,7 @@ exports[`should match snapshot, add new 1`] = `
</span>
</div>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -230,7 +230,7 @@ exports[`should match snapshot, edit title 1`] = `
</span>
</div>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -301,7 +301,7 @@ exports[`should match snapshot, hide group 1`] = `
</span>
</div>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -368,7 +368,7 @@ exports[`should match snapshot, no groups 1`] = `
</div>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -32,7 +32,7 @@ exports[`components/table/TableRow should match snapshot 1`] = `
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -77,7 +77,7 @@ exports[`components/table/TableRow should match snapshot, collapsed tree 1`] = `
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -122,7 +122,7 @@ exports[`components/table/TableRow should match snapshot, display properties 1`]
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -205,7 +205,7 @@ exports[`components/table/TableRow should match snapshot, isSelected 1`] = `
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -251,7 +251,7 @@ exports[`components/table/TableRow should match snapshot, read-only 1`] = `
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -296,7 +296,7 @@ exports[`components/table/TableRow should match snapshot, resizing column 1`] =
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -32,7 +32,7 @@ exports[`components/table/TableRows should match snapshot, fire events 1`] = `
class="open-button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -14,6 +14,8 @@ import {BoardView} from '../../../blocks/boardView'
import {Card} from '../../../blocks/card'
import {Options} from '../../calculations/options'
import {TableCalculationOptions} from './tableCalculationOptions'
type Props = {
board: Board
cards: Card[]
@ -76,6 +78,7 @@ const CalculationRow = (props: Props): JSX.Element => {
cards={props.cards}
property={template}
hovered={hovered}
optionsComponent={TableCalculationOptions}
/>
)
})

View File

@ -0,0 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {CalculationOptions, CommonCalculationOptionProps, optionsByType} from '../../calculations/options'
export const TableCalculationOptions = (props: CommonCalculationOptionProps): JSX.Element => {
const options = [...optionsByType.get('common')!]
if (props.property && optionsByType.get(props.property.type)) {
options.push(...optionsByType.get(props.property.type)!)
}
return (
<CalculationOptions
value={props.value}
menuOpen={props.menuOpen}
onClose={props.onClose}
onChange={props.onChange}
property={props.property}
options={options}
/>
)
}

View File

@ -31,7 +31,7 @@ exports[`components/viewHeader/filterComponent return filterComponent 1`] = `
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -149,7 +149,7 @@ exports[`components/viewHeader/filterComponent return filterComponent 1`] = `
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -161,7 +161,7 @@ exports[`components/viewHeader/filterComponent return filterComponent 1`] = `
class="octo-spacer"
/>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -171,7 +171,7 @@ exports[`components/viewHeader/filterComponent return filterComponent 1`] = `
</div>
<br />
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -214,7 +214,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and add Fi
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -332,7 +332,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and add Fi
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -344,7 +344,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and add Fi
class="octo-spacer"
/>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -354,7 +354,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and add Fi
</div>
<br />
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -397,7 +397,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and click
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -411,7 +411,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and click
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -527,7 +527,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and click
class="octo-spacer"
/>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -537,7 +537,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and click
</div>
<br />
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -580,7 +580,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and filter
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -698,7 +698,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and filter
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -710,7 +710,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and filter
class="octo-spacer"
/>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -720,7 +720,7 @@ exports[`components/viewHeader/filterComponent return filterComponent and filter
</div>
<br />
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -11,7 +11,7 @@ exports[`components/viewHeader/filterEntry return filterEntry 1`] = `
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -129,7 +129,7 @@ exports[`components/viewHeader/filterEntry return filterEntry 1`] = `
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -141,7 +141,7 @@ exports[`components/viewHeader/filterEntry return filterEntry 1`] = `
class="octo-spacer"
/>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -163,7 +163,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on delet
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -177,7 +177,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on delet
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -293,7 +293,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on delet
class="octo-spacer"
/>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -315,7 +315,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on doesn
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -329,7 +329,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on doesn
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -445,7 +445,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on doesn
class="octo-spacer"
/>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -467,7 +467,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on inclu
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -481,7 +481,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on inclu
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -597,7 +597,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on inclu
class="octo-spacer"
/>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -619,7 +619,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on is em
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -633,7 +633,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on is em
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -749,7 +749,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on is em
class="octo-spacer"
/>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -771,7 +771,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on is no
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -785,7 +785,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on is no
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -901,7 +901,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on is no
class="octo-spacer"
/>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -923,7 +923,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on statu
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -1041,7 +1041,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on statu
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -1053,7 +1053,7 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on statu
class="octo-spacer"
/>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -8,7 +8,7 @@ exports[`components/viewHeader/filterValue return filterValue 1`] = `
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -82,7 +82,7 @@ exports[`components/viewHeader/filterValue return filterValue and click Status 1
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -101,7 +101,7 @@ exports[`components/viewHeader/filterValue return filterValue and click Status w
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -35,7 +35,7 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -49,7 +49,7 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -66,7 +66,7 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
class="ModalWrapper"
>
<button
class="Button active "
class="Button active"
type="button"
>
<span>
@ -80,7 +80,7 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
role="button"
>
<button
class="Button active "
class="Button active"
type="button"
>
<span>
@ -89,7 +89,7 @@ exports[`components/viewHeader/viewHeader return viewHeader 1`] = `
</button>
</div>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -171,7 +171,7 @@ exports[`components/viewHeader/viewHeader return viewHeader readonly 1`] = `
class="octo-spacer"
/>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -8,7 +8,7 @@ exports[`components/viewHeader/viewHeaderGroupByMenu return groupBy menu 1`] = `
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -142,7 +142,7 @@ exports[`components/viewHeader/viewHeaderGroupByMenu return groupBy menu and gro
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -166,7 +166,7 @@ exports[`components/viewHeader/viewHeaderGroupByMenu return groupBy menu and ung
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -8,7 +8,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>
@ -139,7 +139,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
role="button"
>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -13,7 +13,7 @@ exports[`components/viewHeader/ViewHeaderSearch return input after click on sear
exports[`components/viewHeader/ViewHeaderSearch return search menu 1`] = `
<div>
<button
class="Button "
class="Button"
type="button"
>
<span>

View File

@ -8,7 +8,7 @@ exports[`components/viewHeader/viewHeaderSortMenu return sort menu 1`] = `
role="button"
>
<button
class="Button active "
class="Button active"
type="button"
>
<span>
@ -194,7 +194,7 @@ exports[`components/viewHeader/viewHeaderSortMenu return sort menu and do Name s
role="button"
>
<button
class="Button active "
class="Button active"
type="button"
>
<span>
@ -213,7 +213,7 @@ exports[`components/viewHeader/viewHeaderSortMenu return sort menu and do manual
role="button"
>
<button
class="Button active "
class="Button active"
type="button"
>
<span>
@ -232,7 +232,7 @@ exports[`components/viewHeader/viewHeaderSortMenu return sort menu and do revert
role="button"
>
<button
class="Button active "
class="Button active"
type="button"
>
<span>

View File

@ -3,7 +3,7 @@
import {BlockIcons} from './blockIcons'
import {Block} from './blocks/block'
import {Board, IPropertyOption, IPropertyTemplate, PropertyType, createBoard} from './blocks/board'
import {BoardView, ISortOption, createBoardView} from './blocks/boardView'
import {BoardView, ISortOption, createBoardView, KanbanCalculationFields} from './blocks/boardView'
import {Card, createCard} from './blocks/card'
import {FilterGroup} from './blocks/filterGroup'
import octoClient, {OctoClient} from './octoClient'
@ -562,6 +562,19 @@ class Mutator {
)
}
async changeViewKanbanCalculations(viewId: string, oldCalculations: Record<string, KanbanCalculationFields>, calculations: Record<string, KanbanCalculationFields>, description = 'updated kanban calculations'): Promise<void> {
await undoManager.perform(
async () => {
await octoClient.patchBlock(viewId, {updatedFields: {kanbanCalculations: calculations}})
},
async () => {
await octoClient.patchBlock(viewId, {updatedFields: {kanbanCalculations: oldCalculations}})
},
description,
this.undoGroupId,
)
}
async hideViewColumn(view: BoardView, columnOptionId: string): Promise<void> {
if (view.fields.hiddenOptionIds.includes(columnOptionId)) {
return

View File

@ -579,6 +579,10 @@ class Utils {
const readToken = queryString.get('r') || ''
return readToken
}
static generateClassName(conditions: Record<string, boolean>): string {
return Object.entries(conditions).map(([className, condition]) => (condition ? className : '')).filter((className) => className !== '').join(' ')
}
}
export {Utils, IDType}

View File

@ -3,9 +3,11 @@
import React from 'react'
import './button.scss'
import {Utils} from '../../utils'
type Props = {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
onBlur?: (e: React.FocusEvent<HTMLButtonElement>) => void
children?: React.ReactNode
title?: string
icon?: React.ReactNode
@ -14,15 +16,26 @@ type Props = {
submit?: boolean
emphasis?: string
size?: string
className?: string
}
function Button(props: Props): JSX.Element {
const classNames: Record<string, boolean> = {
Button: true,
active: Boolean(props.active),
filled: Boolean(props.filled),
}
classNames[`emphasis--${props.emphasis}`] = Boolean(props.emphasis)
classNames[`size--${props.size}`] = Boolean(props.size)
classNames[`${props.className}`] = Boolean(props.className)
return (
<button
type={props.submit ? 'submit' : 'button'}
onClick={props.onClick}
className={`Button ${props.active ? 'active' : ''} ${props.filled ? 'filled' : ''} ${props.emphasis ? 'emphasis--' + props.emphasis : ''} ${props.size ? 'size--' + props.size : ''}`}
className={Utils.generateClassName(classNames)}
title={props.title}
onBlur={props.onBlur}
>
{props.icon}
<span>{props.children}</span>

View File

@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import CompassIcon from './compassIcon'
export default function ChevronRight(): JSX.Element {
return (
<CompassIcon
icon='chevron-right'
className='ChevronRightIcon'
/>
)
}