mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-23 18:34:02 +02:00
Merge branch 'main' into compliance-history-export
This commit is contained in:
commit
533cb9c5a5
@ -172,6 +172,7 @@ if (TARGET_IS_PRODUCT) {
|
||||
|
||||
config.output = {
|
||||
path: path.join(__dirname, '/dist'),
|
||||
chunkFilename: '[name].[contenthash].js',
|
||||
};
|
||||
} else {
|
||||
config.resolve.alias['react-intl'] = path.resolve(__dirname, '../../webapp/node_modules/react-intl/');
|
||||
|
64
webapp/src/blocks/filterClause.test.ts
Normal file
64
webapp/src/blocks/filterClause.test.ts
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {areEqual, createFilterClause} from './filterClause'
|
||||
|
||||
describe('filterClause tests', () => {
|
||||
it('create filter clause', () => {
|
||||
const clause = createFilterClause({
|
||||
propertyId: 'myPropertyId',
|
||||
condition: 'contains',
|
||||
values: [],
|
||||
})
|
||||
|
||||
expect(clause).toEqual({
|
||||
propertyId: 'myPropertyId',
|
||||
condition: 'contains',
|
||||
values: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('test filter clauses are equal', () => {
|
||||
const clause = createFilterClause({
|
||||
propertyId: 'myPropertyId',
|
||||
condition: 'contains',
|
||||
values: ['abc', 'def'],
|
||||
})
|
||||
const newClause = createFilterClause(clause)
|
||||
const testEqual = areEqual(clause, newClause)
|
||||
expect(testEqual).toBeTruthy()
|
||||
})
|
||||
|
||||
it('test filter clauses are Not equal property ID', () => {
|
||||
const clause = createFilterClause({
|
||||
propertyId: 'myPropertyId',
|
||||
condition: 'contains',
|
||||
values: ['abc', 'def'],
|
||||
})
|
||||
const newClause = createFilterClause(clause)
|
||||
newClause.propertyId = 'DifferentID'
|
||||
const testEqual = areEqual(clause, newClause)
|
||||
expect(testEqual).toBeFalsy()
|
||||
})
|
||||
it('test filter clauses are Not equal condition', () => {
|
||||
const clause = createFilterClause({
|
||||
propertyId: 'myPropertyId',
|
||||
condition: 'contains',
|
||||
values: ['abc', 'def'],
|
||||
})
|
||||
const newClause = createFilterClause(clause)
|
||||
newClause.condition = 'notContains'
|
||||
const testEqual = areEqual(clause, newClause)
|
||||
expect(testEqual).toBeFalsy()
|
||||
})
|
||||
it('test filter clauses are Not equal values', () => {
|
||||
const clause = createFilterClause({
|
||||
propertyId: 'myPropertyId',
|
||||
condition: 'contains',
|
||||
values: ['abc', 'def'],
|
||||
})
|
||||
const newClause = createFilterClause(clause)
|
||||
newClause.values = ['abc, def']
|
||||
const testEqual = areEqual(clause, newClause)
|
||||
expect(testEqual).toBeFalsy()
|
||||
})
|
||||
})
|
@ -2,7 +2,15 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {Utils} from '../utils'
|
||||
|
||||
type FilterCondition = 'includes' | 'notIncludes' | 'isEmpty' | 'isNotEmpty' | 'isSet' | 'isNotSet' | 'is' | 'contains' | 'notContains' | 'startsWith' | 'notStartsWith' | 'endsWith' | 'notEndsWith'
|
||||
type FilterCondition =
|
||||
'includes' | 'notIncludes' |
|
||||
'isEmpty' | 'isNotEmpty' |
|
||||
'isSet' | 'isNotSet' |
|
||||
'is' |
|
||||
'contains' | 'notContains' |
|
||||
'startsWith' | 'notStartsWith' |
|
||||
'endsWith' | 'notEndsWith' |
|
||||
'isBefore' | 'isAfter'
|
||||
|
||||
type FilterClause = {
|
||||
propertyId: string
|
||||
|
@ -8,10 +8,14 @@ import {createFilterGroup} from './blocks/filterGroup'
|
||||
import {CardFilter} from './cardFilter'
|
||||
import {TestBlockFactory} from './test/testBlockFactory'
|
||||
import {Utils} from './utils'
|
||||
|
||||
import {IPropertyTemplate} from './blocks/board'
|
||||
|
||||
jest.mock('./utils')
|
||||
const mockedUtils = mocked(Utils, true)
|
||||
|
||||
const dayMillis = 24 * 60 * 60 * 1000
|
||||
|
||||
describe('src/cardFilter', () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
board.id = '1'
|
||||
@ -21,6 +25,7 @@ describe('src/cardFilter', () => {
|
||||
card1.title = 'card1'
|
||||
card1.fields.properties.propertyId = 'Status'
|
||||
const filterClause = createFilterClause({propertyId: 'propertyId', condition: 'isNotEmpty', values: ['Status']})
|
||||
|
||||
describe('verify isClauseMet method', () => {
|
||||
test('should be true with isNotEmpty clause', () => {
|
||||
const filterClauseIsNotEmpty = createFilterClause({propertyId: 'propertyId', condition: 'isNotEmpty', values: ['Status']})
|
||||
@ -53,6 +58,187 @@ describe('src/cardFilter', () => {
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('verify isClauseMet method - single date property', () => {
|
||||
// Date Properties are stored as 12PM UTC.
|
||||
const now = new Date(Date.now())
|
||||
const propertyDate = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 12)
|
||||
|
||||
const dateCard = TestBlockFactory.createCard(board)
|
||||
dateCard.id = '1'
|
||||
dateCard.title = 'card1'
|
||||
dateCard.fields.properties.datePropertyID = '{ "from": ' + propertyDate.toString() + ' }'
|
||||
|
||||
const checkDayBefore = propertyDate - dayMillis
|
||||
const checkDayAfter = propertyDate + dayMillis
|
||||
|
||||
const template: IPropertyTemplate = {
|
||||
id: 'datePropertyID',
|
||||
name: 'myDate',
|
||||
type: 'date',
|
||||
options: [],
|
||||
}
|
||||
|
||||
test('should be true with isSet clause', () => {
|
||||
const filterClauseIsSet = createFilterClause({propertyId: 'datePropertyID', condition: 'isSet', values: []})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsSet, [template], dateCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('should be false with notSet clause', () => {
|
||||
const filterClauseIsNotSet = createFilterClause({propertyId: 'datePropertyID', condition: 'isNotSet', values: []})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsNotSet, [template], dateCard)
|
||||
expect(result).toBeFalsy()
|
||||
})
|
||||
test('verify isBefore clause', () => {
|
||||
const filterClauseIsBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [checkDayAfter.toString()]})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsBefore, [template], dateCard)
|
||||
expect(result).toBeTruthy()
|
||||
|
||||
const filterClauseIsNotBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [checkDayBefore.toString()]})
|
||||
const result2 = CardFilter.isClauseMet(filterClauseIsNotBefore, [template], dateCard)
|
||||
expect(result2).toBeFalsy()
|
||||
})
|
||||
test('verify isAfter clauses', () => {
|
||||
const filterClauseisAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [checkDayBefore.toString()]})
|
||||
const result = CardFilter.isClauseMet(filterClauseisAfter, [template], dateCard)
|
||||
expect(result).toBeTruthy()
|
||||
|
||||
const filterClauseisNotAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [checkDayAfter.toString()]})
|
||||
const result2 = CardFilter.isClauseMet(filterClauseisNotAfter, [template], dateCard)
|
||||
expect(result2).toBeFalsy()
|
||||
})
|
||||
test('verify is clause', () => {
|
||||
const filterClauseIs = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [propertyDate.toString()]})
|
||||
const result = CardFilter.isClauseMet(filterClauseIs, [template], dateCard)
|
||||
expect(result).toBeTruthy()
|
||||
|
||||
const filterClauseIsNot = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [checkDayBefore.toString()]})
|
||||
const result2 = CardFilter.isClauseMet(filterClauseIsNot, [template], dateCard)
|
||||
expect(result2).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('verify isClauseMet method - date range property', () => {
|
||||
// Date Properties are stored as 12PM UTC.
|
||||
const now = new Date(Date.now())
|
||||
const fromDate = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 12)
|
||||
const toDate = fromDate + (2 * dayMillis)
|
||||
const dateCard = TestBlockFactory.createCard(board)
|
||||
dateCard.id = '1'
|
||||
dateCard.title = 'card1'
|
||||
dateCard.fields.properties.datePropertyID = '{ "from": ' + fromDate.toString() + ', "to": ' + toDate.toString() + ' }'
|
||||
|
||||
const beforeRange = fromDate - dayMillis
|
||||
const afterRange = toDate + dayMillis
|
||||
const inRange = fromDate + dayMillis
|
||||
|
||||
const template: IPropertyTemplate = {
|
||||
id: 'datePropertyID',
|
||||
name: 'myDate',
|
||||
type: 'date',
|
||||
options: [],
|
||||
}
|
||||
|
||||
test('verify isBefore clause', () => {
|
||||
const filterClauseIsBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [beforeRange.toString()]})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsBefore, [template], dateCard)
|
||||
expect(result).toBeFalsy()
|
||||
|
||||
const filterClauseIsInRange = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [inRange.toString()]})
|
||||
const result2 = CardFilter.isClauseMet(filterClauseIsInRange, [template], dateCard)
|
||||
expect(result2).toBeTruthy()
|
||||
|
||||
const filterClauseIsAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [afterRange.toString()]})
|
||||
const result3 = CardFilter.isClauseMet(filterClauseIsAfter, [template], dateCard)
|
||||
expect(result3).toBeTruthy()
|
||||
})
|
||||
|
||||
test('verify isAfter clauses', () => {
|
||||
const filterClauseIsAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [afterRange.toString()]})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsAfter, [template], dateCard)
|
||||
expect(result).toBeFalsy()
|
||||
|
||||
const filterClauseIsInRange = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [inRange.toString()]})
|
||||
const result2 = CardFilter.isClauseMet(filterClauseIsInRange, [template], dateCard)
|
||||
expect(result2).toBeTruthy()
|
||||
|
||||
const filterClauseIsBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [beforeRange.toString()]})
|
||||
const result3 = CardFilter.isClauseMet(filterClauseIsBefore, [template], dateCard)
|
||||
expect(result3).toBeTruthy()
|
||||
})
|
||||
|
||||
test('verify is clause', () => {
|
||||
const filterClauseIsBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [beforeRange.toString()]})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsBefore, [template], dateCard)
|
||||
expect(result).toBeFalsy()
|
||||
|
||||
const filterClauseIsInRange = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [inRange.toString()]})
|
||||
const result2 = CardFilter.isClauseMet(filterClauseIsInRange, [template], dateCard)
|
||||
expect(result2).toBeTruthy()
|
||||
|
||||
const filterClauseIsAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [afterRange.toString()]})
|
||||
const result3 = CardFilter.isClauseMet(filterClauseIsAfter, [template], dateCard)
|
||||
expect(result3).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('verify isClauseMet method - (createdTime) date property', () => {
|
||||
const createDate = new Date(card1.createAt)
|
||||
const checkDate = Date.UTC(createDate.getFullYear(), createDate.getMonth(), createDate.getDate(), 12)
|
||||
const checkDayBefore = checkDate - dayMillis
|
||||
const checkDayAfter = checkDate + dayMillis
|
||||
|
||||
const template: IPropertyTemplate = {
|
||||
id: 'datePropertyID',
|
||||
name: 'myDate',
|
||||
type: 'createdTime',
|
||||
options: [],
|
||||
}
|
||||
|
||||
test('should be true with isSet clause', () => {
|
||||
const filterClauseIsSet = createFilterClause({propertyId: 'datePropertyID', condition: 'isSet', values: []})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsSet, [template], card1)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('should be false with notSet clause', () => {
|
||||
const filterClauseIsNotSet = createFilterClause({propertyId: 'datePropertyID', condition: 'isNotSet', values: []})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsNotSet, [template], card1)
|
||||
expect(result).toBeFalsy()
|
||||
})
|
||||
test('verify isBefore clause', () => {
|
||||
const filterClauseIsBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [checkDayAfter.toString()]})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsBefore, [template], card1)
|
||||
expect(result).toBeTruthy()
|
||||
|
||||
const filterClauseIsNotBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [checkDate.toString()]})
|
||||
const result2 = CardFilter.isClauseMet(filterClauseIsNotBefore, [template], card1)
|
||||
expect(result2).toBeFalsy()
|
||||
})
|
||||
test('verify isAfter clauses', () => {
|
||||
const filterClauseisAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [checkDayBefore.toString()]})
|
||||
const result = CardFilter.isClauseMet(filterClauseisAfter, [template], card1)
|
||||
expect(result).toBeTruthy()
|
||||
|
||||
const filterClauseisNotAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [checkDate.toString()]})
|
||||
const result2 = CardFilter.isClauseMet(filterClauseisNotAfter, [template], card1)
|
||||
expect(result2).toBeFalsy()
|
||||
})
|
||||
test('verify is clause', () => {
|
||||
// Is should find on that date regardless of time.
|
||||
const filterClauseIs = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [checkDate.toString()]})
|
||||
const result = CardFilter.isClauseMet(filterClauseIs, [template], card1)
|
||||
expect(result).toBeTruthy()
|
||||
|
||||
const filterClauseIsNot = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [checkDayBefore.toString()]})
|
||||
const result2 = CardFilter.isClauseMet(filterClauseIsNot, [template], card1)
|
||||
expect(result2).toBeFalsy()
|
||||
|
||||
const filterClauseIsNot2 = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [checkDayAfter.toString()]})
|
||||
const result3 = CardFilter.isClauseMet(filterClauseIsNot2, [template], card1)
|
||||
expect(result3).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('verify isFilterGroupMet method', () => {
|
||||
test('should return true with no filter', () => {
|
||||
const filterGroup = createFilterGroup({
|
||||
|
@ -1,12 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {DateUtils} from 'react-day-picker'
|
||||
|
||||
import {DateProperty} from './properties/date/date'
|
||||
|
||||
import {IPropertyTemplate} from './blocks/board'
|
||||
import {Card} from './blocks/card'
|
||||
import {FilterClause} from './blocks/filterClause'
|
||||
import {FilterGroup, isAFilterGroupInstance} from './blocks/filterGroup'
|
||||
import {Utils} from './utils'
|
||||
|
||||
const halfDay = 12 * 60 * 60 * 1000
|
||||
|
||||
class CardFilter {
|
||||
static createDatePropertyFromString(initialValue: string): DateProperty {
|
||||
let dateProperty: DateProperty = {}
|
||||
if (initialValue) {
|
||||
const singleDate = new Date(Number(initialValue))
|
||||
if (singleDate && DateUtils.isDate(singleDate)) {
|
||||
dateProperty.from = singleDate.getTime()
|
||||
} else {
|
||||
try {
|
||||
dateProperty = JSON.parse(initialValue)
|
||||
} catch {
|
||||
//Don't do anything, return empty dateProperty
|
||||
}
|
||||
}
|
||||
}
|
||||
return dateProperty
|
||||
}
|
||||
|
||||
static applyFilterGroup(filterGroup: FilterGroup, templates: readonly IPropertyTemplate[], cards: Card[]): Card[] {
|
||||
return cards.filter((card) => this.isFilterGroupMet(filterGroup, templates, card))
|
||||
}
|
||||
@ -48,6 +71,22 @@ class CardFilter {
|
||||
if (filter.propertyId === 'title') {
|
||||
value = card.title.toLowerCase()
|
||||
}
|
||||
const template = templates.find((o) => o.id === filter.propertyId)
|
||||
let dateValue: DateProperty | undefined
|
||||
if (template?.type === 'date') {
|
||||
dateValue = this.createDatePropertyFromString(value as string)
|
||||
}
|
||||
if (!value) {
|
||||
// const template = templates.find((o) => o.id === filter.propertyId)
|
||||
if (template && template.type === 'createdTime') {
|
||||
value = card.createAt.toString()
|
||||
dateValue = this.createDatePropertyFromString(value as string)
|
||||
} else if (template && template.type === 'updatedTime') {
|
||||
value = card.updateAt.toString()
|
||||
dateValue = this.createDatePropertyFromString(value as string)
|
||||
}
|
||||
}
|
||||
|
||||
switch (filter.condition) {
|
||||
case 'includes': {
|
||||
if (filter.values?.length < 1) {
|
||||
@ -77,6 +116,23 @@ class CardFilter {
|
||||
if (filter.values.length === 0) {
|
||||
return true
|
||||
}
|
||||
if (dateValue !== undefined) {
|
||||
const numericFilter = parseInt(filter.values[0], 10)
|
||||
if (template && (template.type === 'createdTime' || template.type === 'updatedTime')) {
|
||||
// createdTime and updatedTime include the time
|
||||
// So to check if create and/or updated "is" date.
|
||||
// Need to add and subtract 12 hours and check range
|
||||
if (dateValue.from) {
|
||||
return dateValue.from > (numericFilter - halfDay) && dateValue.from < (numericFilter + halfDay)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (dateValue.from && dateValue.to) {
|
||||
return dateValue.from <= numericFilter && dateValue.to >= numericFilter
|
||||
}
|
||||
return dateValue.from === numericFilter
|
||||
}
|
||||
return filter.values[0]?.toLowerCase() === value
|
||||
}
|
||||
case 'contains': {
|
||||
@ -115,6 +171,44 @@ class CardFilter {
|
||||
}
|
||||
return !(value as string || '').endsWith(filter.values[0]?.toLowerCase())
|
||||
}
|
||||
case 'isBefore': {
|
||||
if (dateValue !== undefined) {
|
||||
const numericFilter = parseInt(filter.values[0], 10)
|
||||
if (template && (template.type === 'createdTime' || template.type === 'updatedTime')) {
|
||||
// createdTime and updatedTime include the time
|
||||
// So to check if create and/or updated "isBefore" date.
|
||||
// Need to subtract 12 hours to filter
|
||||
if (dateValue.from) {
|
||||
return dateValue.from < (numericFilter - halfDay)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return dateValue.from ? dateValue.from < numericFilter : false
|
||||
}
|
||||
return false
|
||||
}
|
||||
case 'isAfter': {
|
||||
if (dateValue !== undefined) {
|
||||
const numericFilter = parseInt(filter.values[0], 10)
|
||||
if (template && (template.type === 'createdTime' || template.type === 'updatedTime')) {
|
||||
// createdTime and updatedTime include the time
|
||||
// So to check if create and/or updated "isAfter" date.
|
||||
// Need to add 12 hours to filter
|
||||
if (dateValue.from) {
|
||||
return dateValue.from > (numericFilter + halfDay)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (dateValue.to) {
|
||||
return dateValue.to > numericFilter
|
||||
}
|
||||
return dateValue.from ? dateValue.from > numericFilter : false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
default: {
|
||||
Utils.assertFailure(`Invalid filter condition ${filter.condition}`)
|
||||
}
|
||||
|
@ -2025,22 +2025,6 @@ exports[`components/centerPanel return centerPanel and press touch 1 with readon
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
|
@ -903,21 +903,6 @@ exports[`src/components/workspace return workspace readonly and showcard 1`] = `
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
@ -2029,21 +2014,6 @@ exports[`src/components/workspace should match snapshot with readonly 1`] = `
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
|
@ -9,6 +9,8 @@ import FullCalendar, {EventChangeArg, EventInput, EventContentArg, DayCellConten
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
|
||||
import {DatePropertyType} from '../../properties/types'
|
||||
|
||||
import mutator from '../../mutator'
|
||||
|
||||
import {Board, IPropertyTemplate} from '../../blocks/board'
|
||||
@ -96,20 +98,20 @@ const CalendarFullView = (props: Props): JSX.Element|null => {
|
||||
const myEventsList = useMemo(() => (
|
||||
cards.flatMap((card): EventInput[] => {
|
||||
const property = propsRegistry.get(dateDisplayProperty?.type || 'unknown')
|
||||
|
||||
let dateFrom = new Date(card.createAt || 0)
|
||||
let dateTo = new Date(card.createAt || 0)
|
||||
if (property.isDate && property.getDateFrom && property.getDateTo) {
|
||||
if (property instanceof DatePropertyType) {
|
||||
const dateFromValue = property.getDateFrom(card.fields.properties[dateDisplayProperty?.id || ''], card)
|
||||
if (!dateFromValue) {
|
||||
return []
|
||||
}
|
||||
dateFrom = dateFromValue
|
||||
|
||||
const dateToValue = property.getDateTo(card.fields.properties[dateDisplayProperty?.id || ''], card)
|
||||
if (!dateToValue) {
|
||||
return []
|
||||
}
|
||||
dateTo = dateToValue
|
||||
dateTo = dateToValue || new Date(dateFrom)
|
||||
|
||||
//full calendar end date is exclusive, so increment by 1 day.
|
||||
dateTo.setDate(dateTo.getDate() + 1)
|
||||
}
|
||||
return [{
|
||||
id: card.id,
|
||||
|
@ -77,13 +77,21 @@ const Sidebar = (props: Props) => {
|
||||
const currentBoard = useAppSelector(getCurrentBoard)
|
||||
|
||||
useEffect(() => {
|
||||
wsClient.addOnChange((_: WSClient, categories: Category[]) => {
|
||||
const categoryOnChangeHandler = (_: WSClient, categories: Category[]) => {
|
||||
dispatch(updateCategories(categories))
|
||||
}, 'category')
|
||||
}
|
||||
|
||||
wsClient.addOnChange((_: WSClient, blockCategories: BoardCategoryWebsocketData[]) => {
|
||||
const blockCategoryOnChangeHandler = (_: WSClient, blockCategories: BoardCategoryWebsocketData[]) => {
|
||||
dispatch(updateBoardCategories(blockCategories))
|
||||
}, 'blockCategories')
|
||||
}
|
||||
|
||||
wsClient.addOnChange(categoryOnChangeHandler, 'category')
|
||||
wsClient.addOnChange(blockCategoryOnChangeHandler, 'blockCategories')
|
||||
|
||||
return function cleanup() {
|
||||
wsClient.removeOnChange(categoryOnChangeHandler, 'category')
|
||||
wsClient.removeOnChange(blockCategoryOnChangeHandler, 'blockCategories')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const teamId = useAppSelector(getCurrentTeamId)
|
||||
|
@ -0,0 +1,203 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/viewHeader/dateFilter handle clear 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="DateFilter "
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
June 15
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/dateFilter handles \`Today\` button click event 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="DateFilter empty "
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Empty
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/dateFilter handles calendar click event 1`] = `
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={Object {}}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<DateFilter
|
||||
filter={
|
||||
Object {
|
||||
"condition": "is",
|
||||
"propertyId": "myPropertyId",
|
||||
"values": Array [],
|
||||
}
|
||||
}
|
||||
view={
|
||||
Object {
|
||||
"boardId": "testBoardID",
|
||||
"createAt": 1672228800000,
|
||||
"createdBy": "",
|
||||
"deleteAt": 0,
|
||||
"fields": Object {
|
||||
"cardOrder": Array [
|
||||
"card1",
|
||||
"card2",
|
||||
"card3",
|
||||
],
|
||||
"collapsedOptionIds": Array [],
|
||||
"columnCalculations": Object {},
|
||||
"columnWidths": Object {
|
||||
"column1": 100,
|
||||
"column2": 200,
|
||||
},
|
||||
"dateDisplayPropertyId": undefined,
|
||||
"defaultTemplateId": "",
|
||||
"filter": Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"condition": "is",
|
||||
"propertyId": "myPropertyId",
|
||||
"values": Array [],
|
||||
},
|
||||
],
|
||||
"operation": "and",
|
||||
},
|
||||
"groupById": "property1",
|
||||
"hiddenOptionIds": Array [
|
||||
"value1",
|
||||
],
|
||||
"kanbanCalculations": Object {},
|
||||
"sortOptions": Array [
|
||||
Object {
|
||||
"propertyId": "property1",
|
||||
"reversed": true,
|
||||
},
|
||||
Object {
|
||||
"propertyId": "property2",
|
||||
"reversed": false,
|
||||
},
|
||||
],
|
||||
"viewType": "board",
|
||||
"visibleOptionIds": Array [],
|
||||
"visiblePropertyIds": Array [],
|
||||
},
|
||||
"id": "testViewID",
|
||||
"limited": false,
|
||||
"modifiedBy": "",
|
||||
"parentId": "",
|
||||
"schema": 1,
|
||||
"title": "view title",
|
||||
"type": "view",
|
||||
"updateAt": 1672228800000,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</IntlProvider>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/dateFilter return dateFilter default value 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="DateFilter empty "
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Empty
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/dateFilter return dateFilter invalid value 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="DateFilter "
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Invalid Date
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/dateFilter return dateFilter valid value 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="DateFilter "
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
June 15
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/dateFilter returns local correctly - es local 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="DateFilter "
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
15 de junio
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/dateFilter set via text input 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="DateFilter empty "
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Empty
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -709,6 +709,8 @@ exports[`components/viewHeader/filterComponent return filterComponent and click
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
|
@ -386,6 +386,8 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on delet
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -596,6 +598,8 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on doesn
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -806,6 +810,8 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on inclu
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -1016,6 +1022,8 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on is em
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -1226,6 +1234,8 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on is no
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -1661,6 +1671,8 @@ exports[`components/viewHeader/filterEntry return filterEntry for boolean field
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -1712,6 +1724,302 @@ exports[`components/viewHeader/filterEntry return filterEntry for boolean field
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/filterEntry return filterEntry for date field 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="FilterEntry"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Property 3
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
is
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="DateFilter empty "
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Empty
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Delete
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/filterEntry return filterEntry for date field 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="FilterEntry"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Property 3
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper override menuOpened"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
is
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect bottom "
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
aria-label="is"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
is
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="is before"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
is before
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="is after"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
is after
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="is set"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
is set
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="is not set"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
is not set
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="DateFilter empty "
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Empty
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Delete
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/filterEntry return filterEntry for text field 1`] = `
|
||||
<div>
|
||||
<div
|
||||
@ -1986,6 +2294,8 @@ exports[`components/viewHeader/filterEntry return filterEntry for text field 2`]
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
|
@ -1,5 +1,22 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/viewHeader/filterValue return date filter value 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="DateFilter empty "
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Empty
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/filterValue return filterValue 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
@ -166,22 +166,6 @@ exports[`components/viewHeader/viewHeader return viewHeader readonly 1`] = `
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
|
222
webapp/src/components/viewHeader/dateFilter.scss
Normal file
222
webapp/src/components/viewHeader/dateFilter.scss
Normal file
@ -0,0 +1,222 @@
|
||||
.DateFilter {
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
|
||||
.Editable {
|
||||
width: 50% !important;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
cursor: text;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-height: 24px;
|
||||
width: 100%;
|
||||
margin: 2px;
|
||||
|
||||
&.active {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(var(--body-color), 0.4);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border: 1px solid var(--error-color);
|
||||
border-radius: var(--default-rad);
|
||||
}
|
||||
}
|
||||
|
||||
.Modal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
&.empty .Button {
|
||||
min-height: 24px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.4);
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.Button {
|
||||
// width: calc(100% - 16px);
|
||||
height: 100%;
|
||||
justify-content: left;
|
||||
padding: 0;
|
||||
|
||||
&.--empty {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-option {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
height: 32px;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
>* {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
>*:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
>.menu-name {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
>.SubmenuTriangleIcon {
|
||||
fill: rgba(var(--body-color), 0.7);
|
||||
}
|
||||
|
||||
>.Icon {
|
||||
opacity: 0.56;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
>.IconButton .Icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.DayPicker {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.DayPickerInput-Overlay {
|
||||
background-color: rgba(var(--center-channel-bg-rgb));
|
||||
box-shadow: 0 0 0 1px hsla(0, 0%, 0%, 0.1), 0 4px 11px hsla(0, 0%, 0%, 0.1);
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.DayPicker-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.DayPicker-Month {
|
||||
margin: 0;
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.DayPicker-Body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
max-width: 252px;
|
||||
}
|
||||
|
||||
.DayPicker-Weekdays {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.DayPicker-WeekdaysRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.DayPicker-Weekday {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.DayPicker-NavButton {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.DayPicker-Week {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.DayPicker-Day {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50px;
|
||||
|
||||
&::before {
|
||||
content: unset;
|
||||
}
|
||||
|
||||
&:not(.DayPicker-Day--selected) {
|
||||
&:hover {
|
||||
background: rgba(var(--button-bg-rgb), 0.08) !important;
|
||||
color: rgba(var(--button-bg-rgb), 1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.DayPicker-Day--today {
|
||||
background: transparent;
|
||||
color: #c74655;
|
||||
}
|
||||
|
||||
.DayPicker-Day--start {
|
||||
border-top-left-radius: 50% !important;
|
||||
border-bottom-left-radius: 50% !important;
|
||||
border-top-right-radius: unset;
|
||||
border-bottom-right-radius: unset;
|
||||
}
|
||||
|
||||
.DayPicker-Day--end {
|
||||
border-top-left-radius: unset;
|
||||
border-bottom-left-radius: unset;
|
||||
border-top-right-radius: 50% !important;
|
||||
border-bottom-right-radius: 50% !important;
|
||||
}
|
||||
|
||||
.DayPicker-Day--selected:not(.DayPicker-Day--disabled):not(.DayPicker-Day--outside) {
|
||||
border-radius: unset;
|
||||
background-color: rgb(var(--button-bg-rgb));
|
||||
color: rgba(var(--button-color-rgb), 1);
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--button-bg-rgb)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.DayPicker-Day--selected:not(.DayPicker-Day--start):not(.DayPicker-Day--end):not(.DayPicker-Day--outside) {
|
||||
color: rgb(var(--button-bg-rgb));
|
||||
background-color: rgba(var(--button-bg-rgb), 0.08);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--button-bg-rgb), 0.08) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.DayPicker:not(.DayPicker--interactionDisabled) .DayPicker-Day:not(.DayPicker-Day--disabled):not(.DayPicker-Day--selected):not(.DayPicker-Day--outside):hover {
|
||||
background-color: rgba(var(--body-color), 0.2);
|
||||
}
|
||||
}
|
247
webapp/src/components/viewHeader/dateFilter.test.tsx
Normal file
247
webapp/src/components/viewHeader/dateFilter.test.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
// 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 {IntlProvider} from 'react-intl'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import {wrapIntl} from '../../testUtils'
|
||||
import mutator from '../../mutator'
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
import {createFilterClause, FilterClause} from '../../blocks/filterClause'
|
||||
import {createFilterGroup} from '../../blocks/filterGroup'
|
||||
|
||||
import DateFilter from './dateFilter'
|
||||
|
||||
jest.mock('../../mutator')
|
||||
const mockedMutator = mocked(mutator, true)
|
||||
|
||||
// create Dates for specific days for this year.
|
||||
const June15 = new Date(Date.UTC(new Date().getFullYear(), 5, 15, 12))
|
||||
|
||||
describe('components/viewHeader/dateFilter', () => {
|
||||
const emptyFilterClause = createFilterClause({
|
||||
propertyId: 'myPropertyId',
|
||||
condition: 'is',
|
||||
values: [],
|
||||
})
|
||||
const board = TestBlockFactory.createBoard()
|
||||
board.id = 'testBoardID'
|
||||
|
||||
const activeView = TestBlockFactory.createBoardView(board)
|
||||
const dateFixed = Date.UTC(2022, 11, 28, 12) //Date.parse('28 Dec 2022')
|
||||
activeView.createAt = dateFixed
|
||||
activeView.updateAt = dateFixed
|
||||
|
||||
activeView.id = 'testViewID'
|
||||
activeView.fields.filter = createFilterGroup()
|
||||
activeView.fields.filter.filters = [emptyFilterClause]
|
||||
|
||||
beforeEach(() => {
|
||||
// Quick fix to disregard console error when unmounting a component
|
||||
console.error = jest.fn()
|
||||
document.execCommand = jest.fn()
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
test('return dateFilter default value', () => {
|
||||
const {container} = render(
|
||||
wrapIntl(
|
||||
<DateFilter
|
||||
view={activeView}
|
||||
filter={emptyFilterClause}
|
||||
/>,
|
||||
),
|
||||
)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('return dateFilter invalid value', () => {
|
||||
const {container} = render(
|
||||
wrapIntl(
|
||||
<DateFilter
|
||||
view={activeView}
|
||||
filter={{
|
||||
propertyId: 'myPropertyId',
|
||||
condition: 'is',
|
||||
values: ['string is not valid'],
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('return dateFilter valid value', () => {
|
||||
const june15 = June15.getTime().toString()
|
||||
const {container} = render(
|
||||
wrapIntl(
|
||||
<DateFilter
|
||||
view={activeView}
|
||||
filter={{
|
||||
propertyId: 'myPropertyId',
|
||||
condition: 'is',
|
||||
values: [june15.toString()],
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('returns local correctly - es local', () => {
|
||||
const todayFilterClause = createFilterClause(emptyFilterClause)
|
||||
todayFilterClause.values = [June15.getTime().toString()]
|
||||
activeView.fields.filter = createFilterGroup()
|
||||
activeView.fields.filter.filters = [todayFilterClause]
|
||||
|
||||
const component = (
|
||||
<IntlProvider locale='es'>
|
||||
<DateFilter
|
||||
view={activeView}
|
||||
filter={todayFilterClause}
|
||||
/>
|
||||
</IntlProvider>
|
||||
)
|
||||
|
||||
const {container, getByText} = render(component)
|
||||
const input = getByText('15 de junio')
|
||||
expect(input).not.toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('handles calendar click event', () => {
|
||||
activeView.fields.filter = createFilterGroup()
|
||||
activeView.fields.filter.filters = [emptyFilterClause]
|
||||
|
||||
const component =
|
||||
wrapIntl(
|
||||
<DateFilter
|
||||
view={activeView}
|
||||
filter={emptyFilterClause}
|
||||
/>,
|
||||
)
|
||||
expect(component).toMatchSnapshot()
|
||||
const {getByText, getByTitle} = render(component)
|
||||
|
||||
const dayDisplay = getByText('Empty')
|
||||
userEvent.click(dayDisplay)
|
||||
|
||||
const day = getByText('15')
|
||||
const modal = getByTitle('Close').children[0]
|
||||
userEvent.click(day)
|
||||
userEvent.click(modal)
|
||||
|
||||
const newFilterGroup = createFilterGroup(activeView.fields.filter)
|
||||
const date = new Date()
|
||||
const fifteenth = Date.UTC(date.getFullYear(), date.getMonth(), 15, 12)
|
||||
|
||||
const v = newFilterGroup.filters[0] as FilterClause
|
||||
v.values = [fifteenth.toString()]
|
||||
expect(mockedMutator.changeViewFilter).toHaveBeenCalledWith(board.id, activeView.id, activeView.fields.filter, newFilterGroup)
|
||||
})
|
||||
|
||||
test('handle clear', () => {
|
||||
const todayFilterClause = createFilterClause(emptyFilterClause)
|
||||
todayFilterClause.values = [June15.getTime().toString()]
|
||||
activeView.fields.filter = createFilterGroup()
|
||||
activeView.fields.filter.filters = [todayFilterClause]
|
||||
|
||||
const component =
|
||||
wrapIntl(
|
||||
<DateFilter
|
||||
view={activeView}
|
||||
filter={todayFilterClause}
|
||||
/>,
|
||||
)
|
||||
const {container, getByText, getByTitle} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
// open modal
|
||||
const dayDisplay = getByText('June 15')
|
||||
userEvent.click(dayDisplay)
|
||||
|
||||
const clear = getByText('Clear')
|
||||
const modal = getByTitle('Close').children[0]
|
||||
userEvent.click(clear)
|
||||
userEvent.click(modal)
|
||||
|
||||
const newFilterGroup = createFilterGroup(activeView.fields.filter)
|
||||
const v = newFilterGroup.filters[0] as FilterClause
|
||||
v.values = []
|
||||
expect(mockedMutator.changeViewFilter).toHaveBeenCalledWith(board.id, activeView.id, activeView.fields.filter, newFilterGroup)
|
||||
})
|
||||
|
||||
test('set via text input', () => {
|
||||
activeView.fields.filter = createFilterGroup()
|
||||
activeView.fields.filter.filters = [emptyFilterClause]
|
||||
|
||||
const component =
|
||||
wrapIntl(
|
||||
<DateFilter
|
||||
view={activeView}
|
||||
filter={emptyFilterClause}
|
||||
/>,
|
||||
)
|
||||
|
||||
const {container, getByText, getByTitle, getByPlaceholderText} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
// open modal
|
||||
const dayDisplay = getByText('Empty')
|
||||
userEvent.click(dayDisplay)
|
||||
|
||||
const input = getByPlaceholderText('MM/DD/YYYY')
|
||||
userEvent.type(input, '{selectall}{delay}07/15/2021{enter}')
|
||||
|
||||
const July15 = new Date(Date.UTC(2021, 6, 15, 12))
|
||||
const modal = getByTitle('Close').children[0]
|
||||
userEvent.click(modal)
|
||||
|
||||
const newFilterGroup = createFilterGroup(activeView.fields.filter)
|
||||
const v = newFilterGroup.filters[0] as FilterClause
|
||||
v.values = [July15.getTime().toString()]
|
||||
expect(mockedMutator.changeViewFilter).toHaveBeenCalledWith(board.id, activeView.id, activeView.fields.filter, newFilterGroup)
|
||||
})
|
||||
|
||||
test('handles `Today` button click event', () => {
|
||||
const component =
|
||||
wrapIntl(
|
||||
<DateFilter
|
||||
view={activeView}
|
||||
filter={emptyFilterClause}
|
||||
/>,
|
||||
)
|
||||
|
||||
console.log('handle today')
|
||||
|
||||
const {container, getByText, getByTitle} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
// To see if 'Today' button correctly selects today's date,
|
||||
// we can check it against `new Date()`.
|
||||
// About `Date()`
|
||||
// > "When called as a function, returns a string representation of the current date and time"
|
||||
const date = new Date()
|
||||
const today = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12)
|
||||
|
||||
// open modal
|
||||
const dayDisplay = getByText('Empty')
|
||||
userEvent.click(dayDisplay)
|
||||
|
||||
const day = getByText('Today')
|
||||
const modal = getByTitle('Close').children[0]
|
||||
userEvent.click(day)
|
||||
userEvent.click(modal)
|
||||
|
||||
const newFilterGroup = createFilterGroup(activeView.fields.filter)
|
||||
const v = newFilterGroup.filters[0] as FilterClause
|
||||
v.values = [today.toString()]
|
||||
expect(mockedMutator.changeViewFilter).toHaveBeenCalledWith(board.id, activeView.id, activeView.fields.filter, newFilterGroup)
|
||||
})
|
||||
})
|
204
webapp/src/components/viewHeader/dateFilter.tsx
Normal file
204
webapp/src/components/viewHeader/dateFilter.tsx
Normal file
@ -0,0 +1,204 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState, useCallback} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
import {DateUtils} from 'react-day-picker'
|
||||
import MomentLocaleUtils from 'react-day-picker/moment'
|
||||
import DayPicker from 'react-day-picker/DayPicker'
|
||||
|
||||
import moment from 'moment'
|
||||
|
||||
import mutator from '../../mutator'
|
||||
|
||||
import Editable from '../../widgets/editable'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
|
||||
import Modal from '../../components/modal'
|
||||
import ModalWrapper from '../../components/modalWrapper'
|
||||
import {Utils} from '../../utils'
|
||||
|
||||
import 'react-day-picker/lib/style.css'
|
||||
import './dateFilter.scss'
|
||||
|
||||
import {FilterClause} from '../../blocks/filterClause'
|
||||
import {createFilterGroup} from '../../blocks/filterGroup'
|
||||
|
||||
export type DateProperty = {
|
||||
from?: number
|
||||
to?: number
|
||||
includeTime?: boolean
|
||||
timeZone?: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
view: BoardView
|
||||
filter: FilterClause
|
||||
}
|
||||
|
||||
const loadedLocales: Record<string, moment.Locale> = {}
|
||||
|
||||
function DateFilter(props: Props): JSX.Element {
|
||||
const {filter, view} = props
|
||||
const [showDialog, setShowDialog] = useState(false)
|
||||
|
||||
const filterValue = filter.values
|
||||
|
||||
let dateValue: Date | undefined
|
||||
if (filterValue && filterValue.length > 0) {
|
||||
dateValue = new Date(parseInt(filterValue[0], 10))
|
||||
}
|
||||
|
||||
const [value, setValue] = useState(dateValue)
|
||||
const intl = useIntl()
|
||||
|
||||
const onChange = useCallback((newValue) => {
|
||||
if (value !== newValue) {
|
||||
const adjustedValue = newValue ? new Date(newValue.getTime() - timeZoneOffset(newValue.getTime())) : undefined
|
||||
setValue(adjustedValue)
|
||||
|
||||
const filterIndex = view.fields.filter.filters.indexOf(filter)
|
||||
Utils.assert(filterIndex >= 0, "Can't find filter")
|
||||
|
||||
const filterGroup = createFilterGroup(view.fields.filter)
|
||||
const newFilter = filterGroup.filters[filterIndex] as FilterClause
|
||||
Utils.assert(newFilter, `No filter at index ${filterIndex}`)
|
||||
|
||||
newFilter.values = []
|
||||
if (adjustedValue) {
|
||||
newFilter.values = [adjustedValue.getTime().toString()]
|
||||
}
|
||||
mutator.changeViewFilter(view.boardId, view.id, view.fields.filter, filterGroup)
|
||||
}
|
||||
}, [value, view.boardId, view.id, view.fields.filter])
|
||||
|
||||
const getDisplayDate = (date: Date | null | undefined) => {
|
||||
let displayDate = ''
|
||||
if (date) {
|
||||
displayDate = Utils.displayDate(date, intl)
|
||||
}
|
||||
return displayDate
|
||||
}
|
||||
|
||||
const timeZoneOffset = (date: number): number => {
|
||||
return new Date(date).getTimezoneOffset() * 60 * 1000
|
||||
}
|
||||
|
||||
// Keep date value as UTC, property dates are stored as 12:00 pm UTC
|
||||
// date will need converted to local time, to ensure date stays consistent
|
||||
// dateFrom / dateTo will be used for input and calendar dates
|
||||
const offsetDate = value ? new Date(value.getTime() + timeZoneOffset(value.getTime())) : undefined
|
||||
const [input, setInput] = useState<string>(getDisplayDate(offsetDate))
|
||||
|
||||
const locale = intl.locale.toLowerCase()
|
||||
if (locale && locale !== 'en' && !loadedLocales[locale]) {
|
||||
// eslint-disable-next-line global-require
|
||||
loadedLocales[locale] = require(`moment/locale/${locale}`)
|
||||
}
|
||||
|
||||
const handleTodayClick = (day: Date) => {
|
||||
day.setHours(12)
|
||||
saveValue(day)
|
||||
}
|
||||
|
||||
const handleDayClick = (day: Date) => {
|
||||
saveValue(day)
|
||||
}
|
||||
|
||||
const onClear = () => {
|
||||
saveValue(undefined)
|
||||
}
|
||||
|
||||
const saveValue = (newValue: Date | undefined) => {
|
||||
onChange(newValue)
|
||||
setInput(newValue ? Utils.inputDate(newValue, intl) : '')
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
setShowDialog(false)
|
||||
}
|
||||
|
||||
let displayValue = ''
|
||||
if (offsetDate) {
|
||||
displayValue = getDisplayDate(offsetDate)
|
||||
}
|
||||
|
||||
let buttonText = displayValue
|
||||
if (!buttonText) {
|
||||
buttonText = intl.formatMessage({id: 'DateFilter.empty', defaultMessage: 'Empty'})
|
||||
}
|
||||
|
||||
const className = 'DateFilter'
|
||||
return (
|
||||
<div className={`DateFilter ${displayValue ? '' : 'empty'} `}>
|
||||
<Button
|
||||
onClick={() => setShowDialog(true)}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
|
||||
{showDialog &&
|
||||
<ModalWrapper>
|
||||
<Modal
|
||||
onClose={() => onClose()}
|
||||
>
|
||||
<div
|
||||
className={className + '-overlayWrapper'}
|
||||
>
|
||||
<div className={className + '-overlay'}>
|
||||
<div className={'inputContainer'}>
|
||||
<Editable
|
||||
value={input}
|
||||
placeholderText={moment.localeData(locale).longDateFormat('L')}
|
||||
onFocus={() => {
|
||||
if (offsetDate) {
|
||||
return setInput(Utils.inputDate(offsetDate, intl))
|
||||
}
|
||||
return undefined
|
||||
}}
|
||||
onChange={setInput}
|
||||
onSave={() => {
|
||||
const newDate = MomentLocaleUtils.parseDate(input, 'L', intl.locale)
|
||||
if (newDate && DateUtils.isDate(newDate)) {
|
||||
newDate.setHours(12)
|
||||
saveValue(newDate)
|
||||
} else {
|
||||
setInput(getDisplayDate(offsetDate))
|
||||
}
|
||||
}}
|
||||
onCancel={() => {
|
||||
setInput(getDisplayDate(offsetDate))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DayPicker
|
||||
onDayClick={handleDayClick}
|
||||
initialMonth={offsetDate || new Date()}
|
||||
showOutsideDays={false}
|
||||
locale={locale}
|
||||
localeUtils={MomentLocaleUtils}
|
||||
todayButton={intl.formatMessage({id: 'DateRange.today', defaultMessage: 'Today'})}
|
||||
onTodayButtonClick={handleTodayClick}
|
||||
month={offsetDate}
|
||||
selectedDays={offsetDate}
|
||||
/>
|
||||
<hr/>
|
||||
<div
|
||||
className='MenuOption menu-option'
|
||||
>
|
||||
<Button
|
||||
onClick={onClear}
|
||||
>
|
||||
{intl.formatMessage({id: 'DateRange.clear', defaultMessage: 'Clear'})}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DateFilter
|
@ -27,6 +27,7 @@ const board = TestBlockFactory.createBoard()
|
||||
const activeView = TestBlockFactory.createBoardView(board)
|
||||
board.cardProperties[1].type = 'checkbox'
|
||||
board.cardProperties[2].type = 'text'
|
||||
board.cardProperties[3].type = 'date'
|
||||
const statusFilter: FilterClause = {
|
||||
propertyId: board.cardProperties[0].id,
|
||||
condition: 'includes',
|
||||
@ -42,6 +43,12 @@ const textFilter: FilterClause = {
|
||||
condition: 'contains',
|
||||
values: [],
|
||||
}
|
||||
const dateFilter: FilterClause = {
|
||||
propertyId: board.cardProperties[3].id,
|
||||
condition: 'is',
|
||||
values: [],
|
||||
}
|
||||
|
||||
const unknownFilter: FilterClause = {
|
||||
propertyId: 'unknown',
|
||||
condition: 'includes',
|
||||
@ -122,6 +129,26 @@ describe('components/viewHeader/filterEntry', () => {
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('return filterEntry for date field', () => {
|
||||
activeView.fields.filter.filters = [dateFilter]
|
||||
const {container} = render(
|
||||
wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<FilterEntry
|
||||
board={board}
|
||||
view={activeView}
|
||||
conditionClicked={mockedConditionClicked}
|
||||
filter={dateFilter}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
),
|
||||
)
|
||||
expect(container).toMatchSnapshot()
|
||||
const buttonElement = screen.getAllByRole('button', {name: 'menuwrapper'})[1]
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('return filterEntry and click on status', () => {
|
||||
activeView.fields.filter.filters = [unknownFilter]
|
||||
const {container} = render(
|
||||
|
@ -160,6 +160,37 @@ const FilterEntry = (props: Props): JSX.Element => {
|
||||
onClick={(id) => props.conditionClicked(id, filter)}
|
||||
/>
|
||||
</>}
|
||||
{propertyType.filterValueType === 'date' &&
|
||||
<>
|
||||
<Menu.Text
|
||||
id='is'
|
||||
name={intl.formatMessage({id: 'Filter.is', defaultMessage: 'is'})}
|
||||
onClick={(id) => props.conditionClicked(id, filter)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='isBefore'
|
||||
name={intl.formatMessage({id: 'Filter.isbefore', defaultMessage: 'is before'})}
|
||||
onClick={(id) => props.conditionClicked(id, filter)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='isAfter'
|
||||
name={intl.formatMessage({id: 'Filter.isafter', defaultMessage: 'is after'})}
|
||||
onClick={(id) => props.conditionClicked(id, filter)}
|
||||
/>
|
||||
</>}
|
||||
{propertyType.type === 'date' &&
|
||||
<>
|
||||
<Menu.Text
|
||||
id='isSet'
|
||||
name={intl.formatMessage({id: 'Filter.is-set', defaultMessage: 'is set'})}
|
||||
onClick={(id) => props.conditionClicked(id, filter)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='isNotSet'
|
||||
name={intl.formatMessage({id: 'Filter.is-not-set', defaultMessage: 'is not set'})}
|
||||
onClick={(id) => props.conditionClicked(id, filter)}
|
||||
/>
|
||||
</>}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
<FilterValue
|
||||
|
@ -11,6 +11,7 @@ import userEvent from '@testing-library/user-event'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {FilterClause} from '../../blocks/filterClause'
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
@ -128,4 +129,43 @@ describe('components/viewHeader/filterValue', () => {
|
||||
userEvent.click(switchStatus)
|
||||
expect(switchStatus).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('return date filter value', () => {
|
||||
const propertyTemplate: IPropertyTemplate = {
|
||||
id: 'datePropertyID',
|
||||
name: 'My Date Property',
|
||||
type: 'date',
|
||||
options: [],
|
||||
}
|
||||
board.cardProperties.push(propertyTemplate)
|
||||
|
||||
const dateFilter: FilterClause = {
|
||||
propertyId: 'datePropertyID',
|
||||
condition: 'is',
|
||||
values: [],
|
||||
}
|
||||
|
||||
// filter.values = []
|
||||
activeView.fields.filter.filters = [dateFilter]
|
||||
const {container} = render(
|
||||
wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<FilterValue
|
||||
view={activeView}
|
||||
filter={filter}
|
||||
template={propertyTemplate}
|
||||
propertyType={propsRegistry.get(propertyTemplate.type)}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
),
|
||||
)
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
const buttonElement = screen.getByRole('button', {name: 'Empty'})
|
||||
userEvent.click(buttonElement)
|
||||
|
||||
// make sure modal is displayed
|
||||
const clearButton = screen.getByRole('button', {name: 'Clear'})
|
||||
expect(clearButton).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
@ -1,8 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState} from 'react'
|
||||
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {PropertyType} from '../../properties/types'
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {FilterClause} from '../../blocks/filterClause'
|
||||
import {createFilterGroup} from '../../blocks/filterGroup'
|
||||
@ -13,7 +15,8 @@ import Button from '../../widgets/buttons/button'
|
||||
import Menu from '../../widgets/menu'
|
||||
import Editable from '../../widgets/editable'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import {PropertyType} from '../../properties/types'
|
||||
|
||||
import DateFilter from './dateFilter'
|
||||
|
||||
import './filterValue.scss'
|
||||
|
||||
@ -62,6 +65,19 @@ const filterValue = (props: Props): JSX.Element|null => {
|
||||
)
|
||||
}
|
||||
|
||||
if (propertyType.filterValueType === 'date') {
|
||||
if (filter.condition === 'isSet' || filter.condition === 'isNotSet') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<DateFilter
|
||||
view={view}
|
||||
filter={filter}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
let displayValue: string
|
||||
if (filter.values.length > 0) {
|
||||
displayValue = filter.values.map((id) => {
|
||||
|
@ -149,7 +149,7 @@ const ViewHeader = (props: Props) => {
|
||||
spellCheck={true}
|
||||
autoExpand={false}
|
||||
/>
|
||||
<div>
|
||||
{!props.readonly && (<div>
|
||||
<MenuWrapper label={intl.formatMessage({id: 'ViewHeader.view-menu', defaultMessage: 'View menu'})}>
|
||||
<IconButton icon={<DropdownIcon/>}/>
|
||||
<ViewMenu
|
||||
@ -161,7 +161,8 @@ const ViewHeader = (props: Props) => {
|
||||
/>
|
||||
</MenuWrapper>
|
||||
{showAddViewTourStep && <AddViewTourStep/>}
|
||||
</div>
|
||||
</div>)}
|
||||
|
||||
</div>
|
||||
|
||||
<div className='octo-spacer'/>
|
||||
|
@ -4,6 +4,8 @@
|
||||
import React from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
|
||||
import {DatePropertyType} from '../../properties/types'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import mutator from '../../mutator'
|
||||
@ -27,7 +29,7 @@ const ViewHeaderDisplayByMenu = (props: Props) => {
|
||||
const createdDateName = propsRegistry.get('createdTime').displayName(intl)
|
||||
|
||||
const getDateProperties = (): IPropertyTemplate[] => {
|
||||
return properties?.filter((o: IPropertyTemplate) => propsRegistry.get(o.type).isDate)
|
||||
return properties?.filter((o: IPropertyTemplate) => propsRegistry.get(o.type) instanceof DatePropertyType)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -4,6 +4,8 @@ import React, {useCallback, useEffect, useState} from 'react'
|
||||
import {generatePath, useRouteMatch, useHistory} from 'react-router-dom'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {DatePropertyType} from '../properties/types'
|
||||
|
||||
import {getCurrentBoard, isLoadingBoard, getTemplates} from '../store/boards'
|
||||
import {refreshCards, getCardLimitTimestamp, getCurrentBoardHiddenCardsCount, setLimitTimestamp, getCurrentViewCardsSortedFilteredAndGrouped, setCurrent as setCurrentCard} from '../store/cards'
|
||||
import {
|
||||
@ -122,7 +124,7 @@ function CenterContent(props: Props) {
|
||||
|
||||
let displayProperty = dateDisplayProperty
|
||||
if (!displayProperty && activeView.fields.viewType === 'calendar') {
|
||||
displayProperty = board.cardProperties.find((o) => propsRegistry.get(o.type).isDate)
|
||||
displayProperty = board.cardProperties.find((o) => propsRegistry.get(o.type) instanceof DatePropertyType)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -136,6 +136,17 @@ class OctoUtils {
|
||||
return intl.formatMessage({id: 'Filter.is', defaultMessage: 'is'})
|
||||
}
|
||||
}
|
||||
} else if (filterValueType === 'date') {
|
||||
switch (filterCondition) {
|
||||
case 'is': return intl.formatMessage({id: 'Filter.is', defaultMessage: 'is'})
|
||||
case 'isBefore': return intl.formatMessage({id: 'Filter.is-before', defaultMessage: 'is before'})
|
||||
case 'isAfter': return intl.formatMessage({id: 'Filter.is-after', defaultMessage: 'is after'})
|
||||
case 'isSet': return intl.formatMessage({id: 'Filter.is-set', defaultMessage: 'is set'})
|
||||
case 'isNotSet': return intl.formatMessage({id: 'Filter.is-not-set', defaultMessage: 'is not set'})
|
||||
default: {
|
||||
return intl.formatMessage({id: 'Filter.is', defaultMessage: 'is'})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Utils.assertFailure()
|
||||
return '(unknown)'
|
||||
|
@ -7,15 +7,14 @@ import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {Utils} from '../../utils'
|
||||
|
||||
import {PropertyType, PropertyTypeEnum} from '../types'
|
||||
import {DatePropertyType, PropertyTypeEnum} from '../types'
|
||||
|
||||
import CreatedTime from './createdTime'
|
||||
|
||||
export default class CreatedAtProperty extends PropertyType {
|
||||
export default class CreatedAtProperty extends DatePropertyType {
|
||||
Editor = CreatedTime
|
||||
name = 'Created At'
|
||||
type = 'createdTime' as PropertyTypeEnum
|
||||
isDate = true
|
||||
isReadOnly = true
|
||||
displayName = (intl: IntlShape) => intl.formatMessage({id: 'PropertyType.CreatedTime', defaultMessage: 'Created time'})
|
||||
calculationOptions = [Options.none, Options.count, Options.countEmpty,
|
||||
|
@ -8,21 +8,18 @@ import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {Utils} from '../../utils'
|
||||
|
||||
import {PropertyType, PropertyTypeEnum} from '../types'
|
||||
import {PropertyTypeEnum, DatePropertyType} from '../types'
|
||||
|
||||
import DateComponent, {createDatePropertyFromString} from './date'
|
||||
|
||||
const oneDay = 60 * 60 * 24 * 1000
|
||||
|
||||
const timeZoneOffset = (date: number): number => {
|
||||
return new Date(date).getTimezoneOffset() * 60 * 1000
|
||||
}
|
||||
|
||||
export default class DateProperty extends PropertyType {
|
||||
export default class DateProperty extends DatePropertyType {
|
||||
Editor = DateComponent
|
||||
name = 'Date'
|
||||
type = 'date' as PropertyTypeEnum
|
||||
isDate = true
|
||||
displayName = (intl: IntlShape) => intl.formatMessage({id: 'PropertyType.Date', defaultMessage: 'Date'})
|
||||
calculationOptions = [Options.none, Options.count, Options.countEmpty,
|
||||
Options.countNotEmpty, Options.percentEmpty, Options.percentNotEmpty,
|
||||
@ -65,14 +62,11 @@ export default class DateProperty extends PropertyType {
|
||||
|
||||
getDateTo = (value: string | string[] | undefined) => {
|
||||
const dateProperty = createDatePropertyFromString(value as string)
|
||||
if (!dateProperty.from) {
|
||||
if (!dateProperty.to) {
|
||||
return undefined
|
||||
}
|
||||
const dateFrom = dateProperty.from ? new Date(dateProperty.from + (dateProperty.includeTime ? 0 : timeZoneOffset(dateProperty.from))) : new Date()
|
||||
dateFrom.setHours(0, 0, 0, 0)
|
||||
|
||||
const dateToNumber = dateProperty.to ? dateProperty.to + (dateProperty.includeTime ? 0 : timeZoneOffset(dateProperty.to)) : dateFrom.getTime()
|
||||
const dateTo = new Date(dateToNumber + oneDay) // Add one day.
|
||||
const dateToNumber = dateProperty.to + (dateProperty.includeTime ? 0 : timeZoneOffset(dateProperty.to))
|
||||
const dateTo = new Date(dateToNumber)
|
||||
dateTo.setHours(0, 0, 0, 0)
|
||||
return dateTo
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ function encodeText(text: string): string {
|
||||
|
||||
export type PropertyTypeEnum = BoardPropertyTypeEnum
|
||||
|
||||
export type FilterValueType = 'none'|'options'|'boolean'|'text'
|
||||
export type FilterValueType = 'none'|'options'|'boolean'|'text'|'date'
|
||||
|
||||
export type FilterCondition = {
|
||||
id: string
|
||||
@ -33,7 +33,6 @@ export type PropertyProps = {
|
||||
}
|
||||
|
||||
export abstract class PropertyType {
|
||||
isDate = false
|
||||
canGroup = false
|
||||
canFilter = false
|
||||
filterValueType: FilterValueType = 'none'
|
||||
@ -42,14 +41,10 @@ export abstract class PropertyType {
|
||||
Options.countNotEmpty, Options.percentEmpty, Options.percentNotEmpty,
|
||||
Options.countValue, Options.countUniqueValue]
|
||||
displayValue: (value: string | string[] | undefined, card: Card, template: IPropertyTemplate, intl: IntlShape) => string | string[] | undefined
|
||||
getDateFrom: (value: string | string[] | undefined, card: Card) => Date | undefined
|
||||
getDateTo: (value: string | string[] | undefined, card: Card) => Date | undefined
|
||||
valueLength: (value: string | string[] | undefined, card: Card, template: IPropertyTemplate, intl: IntlShape, fontDescriptor: string, perItemPadding?: number) => number
|
||||
|
||||
constructor() {
|
||||
this.displayValue = (value: string | string[] | undefined) => value
|
||||
this.getDateFrom = () => undefined
|
||||
this.getDateTo = () => undefined
|
||||
this.valueLength = (value: string | string[] | undefined, card: Card, template: IPropertyTemplate, intl: IntlShape, fontDescriptor: string): number => {
|
||||
const displayValue = this.displayValue(value, card, template, intl) || ''
|
||||
return Utils.getTextWidth(displayValue.toString(), fontDescriptor)
|
||||
@ -75,3 +70,16 @@ export abstract class PropertyType {
|
||||
abstract type: PropertyTypeEnum
|
||||
abstract displayName: (intl: IntlShape) => string
|
||||
}
|
||||
|
||||
export abstract class DatePropertyType extends PropertyType {
|
||||
canFilter = true
|
||||
filterValueType: FilterValueType = 'date'
|
||||
getDateFrom: (value: string | string[] | undefined, card: Card) => Date | undefined
|
||||
getDateTo: (value: string | string[] | undefined, card: Card) => Date | undefined
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.getDateFrom = () => undefined
|
||||
this.getDateTo = () => undefined
|
||||
}
|
||||
}
|
||||
|
@ -6,15 +6,14 @@ import {Options} from '../../components/calculations/options'
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {Utils} from '../../utils'
|
||||
import {PropertyType, PropertyTypeEnum} from '../types'
|
||||
import {DatePropertyType, PropertyTypeEnum} from '../types'
|
||||
|
||||
import UpdatedTime from './updatedTime'
|
||||
|
||||
export default class UpdatedTimeProperty extends PropertyType {
|
||||
export default class UpdatedTimeProperty extends DatePropertyType {
|
||||
Editor = UpdatedTime
|
||||
name = 'Last Modified At'
|
||||
type = 'updatedTime' as PropertyTypeEnum
|
||||
isDate = true
|
||||
isReadOnly = true
|
||||
displayName = (intl: IntlShape) => intl.formatMessage({id: 'PropertyType.UpdatedTime', defaultMessage: 'Last updated time'})
|
||||
calculationOptions = [Options.none, Options.count, Options.countEmpty,
|
||||
|
Loading…
x
Reference in New Issue
Block a user