1
0
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:
Mattermost Build 2023-01-13 18:29:42 +02:00 committed by GitHub
commit 533cb9c5a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1737 additions and 101 deletions

View File

@ -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/');

View 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()
})
})

View File

@ -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

View File

@ -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({

View File

@ -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}`)
}

View File

@ -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"

View File

@ -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"

View File

@ -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,

View File

@ -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)

View File

@ -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>
`;

View File

@ -709,6 +709,8 @@ exports[`components/viewHeader/filterComponent return filterComponent and click
</div>
<div />
<div />
<div />
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"

View File

@ -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"

View File

@ -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

View File

@ -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"

View 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);
}
}

View 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)
})
})

View 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

View File

@ -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(

View File

@ -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

View File

@ -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()
})
})

View File

@ -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) => {

View File

@ -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'/>

View File

@ -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 (

View File

@ -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 (

View File

@ -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)'

View File

@ -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,

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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,