mirror of
https://github.com/mattermost/focalboard.git
synced 2025-03-06 15:36:17 +02:00
[GH-1080] Add table functions for dates (#1508)
This adds earliest, latest and range column aggregations for general date as well as created/updated date properties. Fixes: #1080
This commit is contained in:
parent
c9aeeb38bf
commit
6ec084f93d
@ -7,6 +7,7 @@ import {render} from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
import {wrapIntl} from '../../testUtils'
|
||||
|
||||
import Calculation from './calculation'
|
||||
|
||||
@ -24,7 +25,7 @@ describe('components/calculations/Calculation', () => {
|
||||
card2.fields.properties.property_4 = 'Baz'
|
||||
|
||||
test('should match snapshot - none', () => {
|
||||
const component = (
|
||||
const component = wrapIntl(
|
||||
<Calculation
|
||||
style={{}}
|
||||
class={'fooClass'}
|
||||
@ -41,7 +42,7 @@ describe('components/calculations/Calculation', () => {
|
||||
type: 'text',
|
||||
options: [],
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
@ -49,7 +50,7 @@ describe('components/calculations/Calculation', () => {
|
||||
})
|
||||
|
||||
test('should match snapshot - count', () => {
|
||||
const component = (
|
||||
const component = wrapIntl(
|
||||
<Calculation
|
||||
style={{}}
|
||||
class={'fooClass'}
|
||||
@ -66,7 +67,7 @@ describe('components/calculations/Calculation', () => {
|
||||
type: 'text',
|
||||
options: [],
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
@ -74,7 +75,7 @@ describe('components/calculations/Calculation', () => {
|
||||
})
|
||||
|
||||
test('should match snapshot - countValue', () => {
|
||||
const component = (
|
||||
const component = wrapIntl(
|
||||
<Calculation
|
||||
style={{}}
|
||||
class={'fooClass'}
|
||||
@ -91,7 +92,7 @@ describe('components/calculations/Calculation', () => {
|
||||
type: 'text',
|
||||
options: [],
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
@ -99,7 +100,7 @@ describe('components/calculations/Calculation', () => {
|
||||
})
|
||||
|
||||
test('should match snapshot - countUniqueValue', () => {
|
||||
const component = (
|
||||
const component = wrapIntl(
|
||||
<Calculation
|
||||
style={{}}
|
||||
class={'fooClass'}
|
||||
@ -116,7 +117,7 @@ describe('components/calculations/Calculation', () => {
|
||||
type: 'text',
|
||||
options: [],
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
@ -128,7 +129,7 @@ describe('components/calculations/Calculation', () => {
|
||||
const onMenuClose = jest.fn()
|
||||
const onChange = jest.fn()
|
||||
|
||||
const component = (
|
||||
const component = wrapIntl(
|
||||
<Calculation
|
||||
style={{}}
|
||||
class={'fooClass'}
|
||||
@ -145,7 +146,7 @@ describe('components/calculations/Calculation', () => {
|
||||
type: 'text',
|
||||
options: [],
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {CSSProperties} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {Card} from '../../blocks/card'
|
||||
|
||||
@ -29,6 +30,7 @@ type Props = {
|
||||
const Calculation = (props: Props): JSX.Element => {
|
||||
const value = props.value || Options.none.value
|
||||
const valueOption = Options[value]
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
|
||||
@ -68,7 +70,7 @@ const Calculation = (props: Props): JSX.Element => {
|
||||
{
|
||||
value !== Options.none.value &&
|
||||
<span className='calculationValue'>
|
||||
{Calculations[value] ? Calculations[value](props.cards, props.property) : ''}
|
||||
{Calculations[value] ? Calculations[value](props.cards, props.property, intl) : ''}
|
||||
</span>
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createIntl} from 'react-intl'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
@ -149,10 +151,12 @@ describe('components/calculations/calculation logic', () => {
|
||||
updatedBy: {id: 'property_lastUpdatedBy', type: 'updatedBy', name: '', options: []},
|
||||
}
|
||||
|
||||
const intl = createIntl({locale: 'en-us'})
|
||||
|
||||
// testing count
|
||||
Object.values(properties).forEach((property) => {
|
||||
it(`should correctly count for property type "${property.type}"`, function() {
|
||||
expect(Calculations.count(cards, property)).toBe('4')
|
||||
expect(Calculations.count(cards, property, intl)).toBe('4')
|
||||
})
|
||||
})
|
||||
|
||||
@ -175,7 +179,7 @@ describe('components/calculations/calculation logic', () => {
|
||||
}
|
||||
Object.keys(countValueTests).forEach((propertyType) => {
|
||||
it(`should correctly count values for property type ${propertyType}`, function() {
|
||||
expect(Calculations.countValue(cards, properties[propertyType]!)).toBe(countValueTests[propertyType]!)
|
||||
expect(Calculations.countValue(cards, properties[propertyType]!, intl)).toBe(countValueTests[propertyType]!)
|
||||
})
|
||||
})
|
||||
|
||||
@ -198,97 +202,97 @@ describe('components/calculations/calculation logic', () => {
|
||||
}
|
||||
Object.keys(countUniqueValueTests).forEach((propertyType) => {
|
||||
it(`should correctly count unique values for property type ${propertyType}`, function() {
|
||||
expect(Calculations.countUniqueValue(cards, properties[propertyType]!)).toBe(countUniqueValueTests[propertyType]!)
|
||||
expect(Calculations.countUniqueValue(cards, properties[propertyType]!, intl)).toBe(countUniqueValueTests[propertyType]!)
|
||||
})
|
||||
})
|
||||
|
||||
test('countUniqueValue for cards created 1 second apart', () => {
|
||||
const result = Calculations.countUniqueValue([card3, card6], properties.createdTime)
|
||||
const result = Calculations.countUniqueValue([card3, card6], properties.createdTime, intl)
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
test('countUniqueValue for cards updated 1 second apart', () => {
|
||||
const result = Calculations.countUniqueValue([card3, card6], properties.updatedTime)
|
||||
const result = Calculations.countUniqueValue([card3, card6], properties.updatedTime, intl)
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
test('countUniqueValue for cards created 1 minute apart', () => {
|
||||
const result = Calculations.countUniqueValue([card3, card7], properties.createdTime)
|
||||
const result = Calculations.countUniqueValue([card3, card7], properties.createdTime, intl)
|
||||
expect(result).toBe('2')
|
||||
})
|
||||
|
||||
test('countUniqueValue for cards updated 1 minute apart', () => {
|
||||
const result = Calculations.countUniqueValue([card3, card7], properties.updatedTime)
|
||||
const result = Calculations.countUniqueValue([card3, card7], properties.updatedTime, intl)
|
||||
expect(result).toBe('2')
|
||||
})
|
||||
|
||||
test('countChecked for cards', () => {
|
||||
const result = Calculations.countChecked(cards, properties.checkbox)
|
||||
const result = Calculations.countChecked(cards, properties.checkbox, intl)
|
||||
expect(result).toBe('3')
|
||||
})
|
||||
|
||||
test('countChecked for cards, one set, other unset', () => {
|
||||
const result = Calculations.countChecked([card1, card5], properties.checkbox)
|
||||
const result = Calculations.countChecked([card1, card5], properties.checkbox, intl)
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
test('countUnchecked for cards', () => {
|
||||
const result = Calculations.countUnchecked(cards, properties.checkbox)
|
||||
const result = Calculations.countUnchecked(cards, properties.checkbox, intl)
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
test('countUnchecked for cards, two set, one unset', () => {
|
||||
const result = Calculations.countUnchecked([card1, card1, card5], properties.checkbox)
|
||||
const result = Calculations.countUnchecked([card1, card1, card5], properties.checkbox, intl)
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
test('countUnchecked for cards, one set, other unset', () => {
|
||||
const result = Calculations.countUnchecked([card1, card5], properties.checkbox)
|
||||
const result = Calculations.countUnchecked([card1, card5], properties.checkbox, intl)
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
test('countUnchecked for cards, one set, two unset', () => {
|
||||
const result = Calculations.countUnchecked([card1, card5, card5], properties.checkbox)
|
||||
const result = Calculations.countUnchecked([card1, card5, card5], properties.checkbox, intl)
|
||||
expect(result).toBe('2')
|
||||
})
|
||||
|
||||
test('percentChecked for cards', () => {
|
||||
const result = Calculations.percentChecked(cards, properties.checkbox)
|
||||
const result = Calculations.percentChecked(cards, properties.checkbox, intl)
|
||||
expect(result).toBe('75%')
|
||||
})
|
||||
|
||||
test('percentUnchecked for cards', () => {
|
||||
const result = Calculations.percentUnchecked(cards, properties.checkbox)
|
||||
const result = Calculations.percentUnchecked(cards, properties.checkbox, intl)
|
||||
expect(result).toBe('25%')
|
||||
})
|
||||
|
||||
test('sum', () => {
|
||||
const result = Calculations.sum(cards, properties.number)
|
||||
const result = Calculations.sum(cards, properties.number, intl)
|
||||
expect(result).toBe('170')
|
||||
})
|
||||
|
||||
test('average', () => {
|
||||
const result = Calculations.average(cards, properties.number)
|
||||
const result = Calculations.average(cards, properties.number, intl)
|
||||
expect(result).toBe('56.67')
|
||||
})
|
||||
|
||||
test('median', () => {
|
||||
const result = Calculations.median(cards, properties.number)
|
||||
const result = Calculations.median(cards, properties.number, intl)
|
||||
expect(result).toBe('100')
|
||||
})
|
||||
|
||||
test('min', () => {
|
||||
const result = Calculations.min(cards, properties.number)
|
||||
const result = Calculations.min(cards, properties.number, intl)
|
||||
expect(result).toBe('-30')
|
||||
})
|
||||
|
||||
test('max', () => {
|
||||
const result = Calculations.max(cards, properties.number)
|
||||
const result = Calculations.max(cards, properties.number, intl)
|
||||
expect(result).toBe('100')
|
||||
})
|
||||
|
||||
test('range', () => {
|
||||
const result = Calculations.range(cards, properties.number)
|
||||
const result = Calculations.range(cards, properties.number, intl)
|
||||
expect(result).toBe('-30 - 100')
|
||||
})
|
||||
})
|
||||
|
@ -1,10 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntlShape} from 'react-intl'
|
||||
|
||||
import moment from 'moment'
|
||||
|
||||
import {Card} from '../../blocks/card'
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {Utils} from '../../utils'
|
||||
import {Constants} from '../../constants'
|
||||
import {DateProperty} from '../properties/dateRange/dateRange'
|
||||
|
||||
const ROUNDED_DECIMAL_PLACES = 2
|
||||
|
||||
@ -197,7 +202,77 @@ function range(cards: readonly Card[], property: IPropertyTemplate): string {
|
||||
return min(cards, property) + ' - ' + max(cards, property)
|
||||
}
|
||||
|
||||
const Calculations: Record<string, (cards: readonly Card[], property: IPropertyTemplate) => string> = {
|
||||
function earliest(cards: readonly Card[], property: IPropertyTemplate, intl: IntlShape): string {
|
||||
const result = earliestEpoch(cards, property)
|
||||
if (result === Number.POSITIVE_INFINITY) {
|
||||
return ''
|
||||
}
|
||||
const date = new Date(result)
|
||||
return property.type === 'date' ? Utils.displayDate(date, intl) : Utils.displayDateTime(date, intl)
|
||||
}
|
||||
|
||||
function earliestEpoch(cards: readonly Card[], property: IPropertyTemplate): number {
|
||||
let result = Number.POSITIVE_INFINITY
|
||||
cards.forEach((card) => {
|
||||
const timestamps = getTimestampsFromPropertyValue(getCardProperty(card, property))
|
||||
for (const timestamp of timestamps) {
|
||||
result = Math.min(result, timestamp)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function latest(cards: readonly Card[], property: IPropertyTemplate, intl: IntlShape): string {
|
||||
const result = latestEpoch(cards, property)
|
||||
if (result === Number.NEGATIVE_INFINITY) {
|
||||
return ''
|
||||
}
|
||||
const date = new Date(result)
|
||||
return property.type === 'date' ? Utils.displayDate(date, intl) : Utils.displayDateTime(date, intl)
|
||||
}
|
||||
|
||||
function latestEpoch(cards: readonly Card[], property: IPropertyTemplate): number {
|
||||
let result = Number.NEGATIVE_INFINITY
|
||||
cards.forEach((card) => {
|
||||
const timestamps = getTimestampsFromPropertyValue(getCardProperty(card, property))
|
||||
for (const timestamp of timestamps) {
|
||||
result = Math.max(result, timestamp)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function getTimestampsFromPropertyValue(value: number | string | string[]): number[] {
|
||||
if (typeof value === 'number') {
|
||||
return [value]
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
let property: DateProperty
|
||||
try {
|
||||
property = JSON.parse(value)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
return [property.from, property.to].flatMap((e) => {
|
||||
return e ? [e] : []
|
||||
})
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function dateRange(cards: readonly Card[], property: IPropertyTemplate): string {
|
||||
const resultEarliest = earliestEpoch(cards, property)
|
||||
if (resultEarliest === Number.POSITIVE_INFINITY) {
|
||||
return ''
|
||||
}
|
||||
const resultLatest = latestEpoch(cards, property)
|
||||
if (resultLatest === Number.NEGATIVE_INFINITY) {
|
||||
return ''
|
||||
}
|
||||
return moment.duration(resultLatest - resultEarliest, 'milliseconds').humanize()
|
||||
}
|
||||
|
||||
const Calculations: Record<string, (cards: readonly Card[], property: IPropertyTemplate, intl: IntlShape) => string> = {
|
||||
count,
|
||||
countValue,
|
||||
countUniqueValue,
|
||||
@ -211,6 +286,9 @@ const Calculations: Record<string, (cards: readonly Card[], property: IPropertyT
|
||||
min,
|
||||
max,
|
||||
range,
|
||||
earliest,
|
||||
latest,
|
||||
dateRange,
|
||||
}
|
||||
|
||||
export default Calculations
|
||||
|
@ -31,12 +31,18 @@ const Options:Record<string, Option> = {
|
||||
min: {value: 'min', label: 'Min', displayName: 'Min'},
|
||||
max: {value: 'max', label: 'Max', displayName: 'Max'},
|
||||
range: {value: 'range', label: 'Range', displayName: 'Range'},
|
||||
earliest: {value: 'earliest', label: 'Earliest', displayName: 'Earliest'},
|
||||
latest: {value: 'latest', label: 'Latest', displayName: 'Latest'},
|
||||
dateRange: {value: 'dateRange', label: 'Range', displayName: 'Range'},
|
||||
}
|
||||
|
||||
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]],
|
||||
['date', [Options.earliest, Options.latest, Options.dateRange]],
|
||||
['createdTime', [Options.earliest, Options.latest, Options.dateRange]],
|
||||
['updatedTime', [Options.earliest, Options.latest, Options.dateRange]],
|
||||
])
|
||||
|
||||
const baseStyles = getSelectBaseStyle()
|
||||
|
@ -26,7 +26,7 @@ type Props = {
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
type DateProperty = {
|
||||
export type DateProperty = {
|
||||
from?: number
|
||||
to?: number
|
||||
includeTime?: boolean
|
||||
|
Loading…
x
Reference in New Issue
Block a user