From 820cae6725a34ca813743436fc0727e3790eb8cc Mon Sep 17 00:00:00 2001 From: redhoyasa Date: Mon, 31 Oct 2022 14:52:47 +0700 Subject: [PATCH 1/8] make view list disabled for readonly mode --- webapp/src/components/viewMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/src/components/viewMenu.tsx b/webapp/src/components/viewMenu.tsx index acb1dbf38..1d1536c35 100644 --- a/webapp/src/components/viewMenu.tsx +++ b/webapp/src/components/viewMenu.tsx @@ -279,6 +279,7 @@ const ViewMenu = (props: Props) => { name={view.title} icon={iconForViewType(view.fields.viewType)} onClick={handleViewClick} + disabled={props.readonly} />))} From 1da0abd7180a15b087185b7cbd72fce83101ec6c Mon Sep 17 00:00:00 2001 From: redhoyasa Date: Mon, 7 Nov 2022 22:04:56 +0700 Subject: [PATCH 2/8] disable menu dropdown for readonly mode (#2932) --- webapp/src/components/viewHeader/viewHeader.tsx | 5 ++++- webapp/src/components/viewMenu.tsx | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/viewHeader/viewHeader.tsx b/webapp/src/components/viewHeader/viewHeader.tsx index 95dc19652..2b7957cb2 100644 --- a/webapp/src/components/viewHeader/viewHeader.tsx +++ b/webapp/src/components/viewHeader/viewHeader.tsx @@ -150,7 +150,10 @@ const ViewHeader = (props: Props) => { autoExpand={false} />
- + }/> { name={view.title} icon={iconForViewType(view.fields.viewType)} onClick={handleViewClick} - disabled={props.readonly} />))}
From eb031cab5fb96594819f21d7f6d3f01891fa98cb Mon Sep 17 00:00:00 2001 From: redhoyasa Date: Fri, 9 Dec 2022 16:41:36 +0700 Subject: [PATCH 3/8] hide dropdown for readonly mode --- webapp/src/components/viewHeader/viewHeader.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/webapp/src/components/viewHeader/viewHeader.tsx b/webapp/src/components/viewHeader/viewHeader.tsx index 2b7957cb2..d8d5b4e1b 100644 --- a/webapp/src/components/viewHeader/viewHeader.tsx +++ b/webapp/src/components/viewHeader/viewHeader.tsx @@ -149,11 +149,8 @@ const ViewHeader = (props: Props) => { spellCheck={true} autoExpand={false} /> -
- + {!props.readonly && (
+ }/> { /> {showAddViewTourStep && } -
+
)} +
From 9e9b67b813dca75244f0006e3d7b60101e7a20ad Mon Sep 17 00:00:00 2001 From: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Date: Tue, 10 Jan 2023 19:05:26 +0530 Subject: [PATCH 4/8] Added unregister change handler to useEffect (#4373) Co-authored-by: Mattermost Build --- webapp/src/components/sidebar/sidebar.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/webapp/src/components/sidebar/sidebar.tsx b/webapp/src/components/sidebar/sidebar.tsx index a8509ef01..558cdd0b4 100644 --- a/webapp/src/components/sidebar/sidebar.tsx +++ b/webapp/src/components/sidebar/sidebar.tsx @@ -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) From 5450a298adfe587dca43778967b058667daf8b26 Mon Sep 17 00:00:00 2001 From: Rajat-Dabade Date: Wed, 11 Jan 2023 23:52:51 +0530 Subject: [PATCH 5/8] Updated snapshot --- .../__snapshots__/centerPanel.test.tsx.snap | 16 ---------- .../__snapshots__/workspace.test.tsx.snap | 30 ------------------- .../__snapshots__/viewHeader.test.tsx.snap | 16 ---------- 3 files changed, 62 deletions(-) diff --git a/webapp/src/components/__snapshots__/centerPanel.test.tsx.snap b/webapp/src/components/__snapshots__/centerPanel.test.tsx.snap index 49bf19dea..96d6ec147 100644 --- a/webapp/src/components/__snapshots__/centerPanel.test.tsx.snap +++ b/webapp/src/components/__snapshots__/centerPanel.test.tsx.snap @@ -2025,22 +2025,6 @@ exports[`components/centerPanel return centerPanel and press touch 1 with readon title="view title" value="view title" /> -
- -
-
- -
-
- -
-
- -
Date: Wed, 11 Jan 2023 16:48:08 -0700 Subject: [PATCH 6/8] Implement Date Filter (#4412) * implement a date property type * handle when property is not set, default create date * update to remove unneeded fields, updated how dateTo is returned * fix creating new date * remove unused constant * update unit tests * implement date filter * add unit tests for all files * some cleanup * fix unit tests, update snapshots * date fix for unit tests * add log to determine unit test issue * update snapshot * minor cleanup * add class name * Revert "add class name" This reverts commit fec93088122c2fc42aeb8ffaf7ba12b6d21a52a4. * add class name Co-authored-by: Mattermod Co-authored-by: Mattermost Build --- webapp/src/blocks/filterClause.test.ts | 64 ++++ webapp/src/blocks/filterClause.ts | 10 +- webapp/src/cardFilter.test.ts | 186 +++++++++++ webapp/src/cardFilter.ts | 94 ++++++ .../src/components/calendar/fullCalendar.tsx | 14 +- .../__snapshots__/dateFilter.test.tsx.snap | 203 ++++++++++++ .../filterComponent.test.tsx.snap | 2 + .../__snapshots__/filterEntry.test.tsx.snap | 310 ++++++++++++++++++ .../__snapshots__/filterValue.test.tsx.snap | 17 + .../src/components/viewHeader/dateFilter.scss | 222 +++++++++++++ .../components/viewHeader/dateFilter.test.tsx | 247 ++++++++++++++ .../src/components/viewHeader/dateFilter.tsx | 204 ++++++++++++ .../viewHeader/filterEntry.test.tsx | 27 ++ .../src/components/viewHeader/filterEntry.tsx | 31 ++ .../viewHeader/filterValue.test.tsx | 40 +++ .../src/components/viewHeader/filterValue.tsx | 18 +- .../viewHeader/viewHeaderDisplayByMenu.tsx | 4 +- webapp/src/components/workspace.tsx | 4 +- webapp/src/octoUtils.tsx | 11 + .../src/properties/createdTime/property.tsx | 5 +- webapp/src/properties/date/property.tsx | 16 +- webapp/src/properties/types.tsx | 20 +- .../src/properties/updatedTime/property.tsx | 5 +- 23 files changed, 1721 insertions(+), 33 deletions(-) create mode 100644 webapp/src/blocks/filterClause.test.ts create mode 100644 webapp/src/components/viewHeader/__snapshots__/dateFilter.test.tsx.snap create mode 100644 webapp/src/components/viewHeader/dateFilter.scss create mode 100644 webapp/src/components/viewHeader/dateFilter.test.tsx create mode 100644 webapp/src/components/viewHeader/dateFilter.tsx diff --git a/webapp/src/blocks/filterClause.test.ts b/webapp/src/blocks/filterClause.test.ts new file mode 100644 index 000000000..e55660564 --- /dev/null +++ b/webapp/src/blocks/filterClause.test.ts @@ -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() + }) +}) diff --git a/webapp/src/blocks/filterClause.ts b/webapp/src/blocks/filterClause.ts index 166b05c24..bffcf79f8 100644 --- a/webapp/src/blocks/filterClause.ts +++ b/webapp/src/blocks/filterClause.ts @@ -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 diff --git a/webapp/src/cardFilter.test.ts b/webapp/src/cardFilter.test.ts index 9f82add41..d3ea5c408 100644 --- a/webapp/src/cardFilter.test.ts +++ b/webapp/src/cardFilter.test.ts @@ -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({ diff --git a/webapp/src/cardFilter.ts b/webapp/src/cardFilter.ts index 4bd41e39c..d6fac950a 100644 --- a/webapp/src/cardFilter.ts +++ b/webapp/src/cardFilter.ts @@ -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}`) } diff --git a/webapp/src/components/calendar/fullCalendar.tsx b/webapp/src/components/calendar/fullCalendar.tsx index 8cf0bb016..7c1fccf26 100644 --- a/webapp/src/components/calendar/fullCalendar.tsx +++ b/webapp/src/components/calendar/fullCalendar.tsx @@ -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, diff --git a/webapp/src/components/viewHeader/__snapshots__/dateFilter.test.tsx.snap b/webapp/src/components/viewHeader/__snapshots__/dateFilter.test.tsx.snap new file mode 100644 index 000000000..d450a05ad --- /dev/null +++ b/webapp/src/components/viewHeader/__snapshots__/dateFilter.test.tsx.snap @@ -0,0 +1,203 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/viewHeader/dateFilter handle clear 1`] = ` +
+
+ +
+
+`; + +exports[`components/viewHeader/dateFilter handles \`Today\` button click event 1`] = ` +
+
+ +
+
+`; + +exports[`components/viewHeader/dateFilter handles calendar click event 1`] = ` + + + +`; + +exports[`components/viewHeader/dateFilter return dateFilter default value 1`] = ` +
+
+ +
+
+`; + +exports[`components/viewHeader/dateFilter return dateFilter invalid value 1`] = ` +
+
+ +
+
+`; + +exports[`components/viewHeader/dateFilter return dateFilter valid value 1`] = ` +
+
+ +
+
+`; + +exports[`components/viewHeader/dateFilter returns local correctly - es local 1`] = ` +
+
+ +
+
+`; + +exports[`components/viewHeader/dateFilter set via text input 1`] = ` +
+
+ +
+
+`; diff --git a/webapp/src/components/viewHeader/__snapshots__/filterComponent.test.tsx.snap b/webapp/src/components/viewHeader/__snapshots__/filterComponent.test.tsx.snap index 87b1138c8..fb3f2c81f 100644 --- a/webapp/src/components/viewHeader/__snapshots__/filterComponent.test.tsx.snap +++ b/webapp/src/components/viewHeader/__snapshots__/filterComponent.test.tsx.snap @@ -709,6 +709,8 @@ exports[`components/viewHeader/filterComponent return filterComponent and click
+
+