From c9b4e75875231fd04a5fc62ca1d23cfdb0a1d97c Mon Sep 17 00:00:00 2001 From: Alessandro Amantini Date: Tue, 4 Feb 2025 17:44:51 +0000 Subject: [PATCH 1/3] ISSUE #5374 - set default tickets filters --- .../store/tickets/card/ticketsCard.redux.ts | 11 ++ .../src/v5/ui/controls/chip/chip.types.tsx | 2 +- .../defaultTicketFiltersSetter.component.tsx | 109 ++++++++++++++++++ ...andleTicketsCardSearchParams.component.tsx | 73 ------------ frontend/src/v5/ui/routes/viewer/viewer.tsx | 4 +- .../tickets/card/ticketsCard.store.spec.ts | 6 + 6 files changed, 129 insertions(+), 76 deletions(-) create mode 100644 frontend/src/v5/ui/routes/viewer/defaultTicketFiltersSetter/defaultTicketFiltersSetter.component.tsx delete mode 100644 frontend/src/v5/ui/routes/viewer/handleTicketsCardSearchParams/handleTicketsCardSearchParams.component.tsx diff --git a/frontend/src/v5/store/tickets/card/ticketsCard.redux.ts b/frontend/src/v5/store/tickets/card/ticketsCard.redux.ts index 16d0fd68361..4cdcbbeb486 100644 --- a/frontend/src/v5/store/tickets/card/ticketsCard.redux.ts +++ b/frontend/src/v5/store/tickets/card/ticketsCard.redux.ts @@ -28,6 +28,7 @@ export const { Types: TicketsCardTypes, Creators: TicketsCardActions } = createA setSelectedTicket: ['ticketId'], setSelectedTemplate: ['templateId'], setSelectedTicketPin: ['pinId'], + setFilters: ['filters'], upsertFilter: ['filter'], deleteFilter: ['filter'], resetFilters: [], @@ -93,6 +94,13 @@ export const setPinToDrop = (state: ITicketsCardState, { pinToDrop }: SetPinToDr }; const getFilterKey = ({ module, property, type }: CardFilter): TicketFilterKey => `${module}.${property}.${type}`; +export const setFilters = (state: ITicketsCardState, { filters }: SetFiltersAction) => { + filters.forEach((filter) => { + const path = getFilterKey(filter); + state.filters[path] = filter.filter; + }); +}; + export const upsertFilter = (state: ITicketsCardState, { filter }: UpsertFilterAction) => { const path = getFilterKey(filter); state.filters[path] = filter.filter; @@ -144,6 +152,7 @@ export const ticketsCardReducer = createReducer(INITIAL_STATE, produceAll({ [TicketsCardTypes.SET_SELECTED_TEMPLATE]: setSelectedTemplate, [TicketsCardTypes.SET_SELECTED_TICKET_PIN]: setSelectedTicketPin, [TicketsCardTypes.SET_PIN_TO_DROP]: setPinToDrop, + [TicketsCardTypes.SET_FILTERS]: setFilters, [TicketsCardTypes.UPSERT_FILTER]: upsertFilter, [TicketsCardTypes.DELETE_FILTER]: deleteFilter, [TicketsCardTypes.RESET_FILTERS]: resetFilters, @@ -160,6 +169,7 @@ export type SetSelectedTicketAction = Action<'SET_SELECTED_TICKET'> & { ticketId export type SetSelectedTemplateAction = Action<'SET_SELECTED_TEMPLATE'> & { templateId: string }; export type SetSelectedTicketPinAction = Action<'SET_SELECTED_TICKET_PIN'> & { pinId: string }; export type SetPinToDropAction = Action<'SET_PIN_TO_DROP'> & { pinToDrop: string }; +export type SetFiltersAction = Action<'SET_FILTERS'> & { filters: CardFilter[] }; export type UpsertFilterAction = Action<'UPSERT_FILTER'> & { filter: CardFilter }; export type DeleteFilterAction = Action<'DELETE_FILTER'> & { filter: CardFilter }; export type ResetFiltersAction = Action<'RESET_FILTERS'>; @@ -179,6 +189,7 @@ export interface ITicketsCardActionCreators { setSelectedTemplate: (templateId: string) => SetSelectedTemplateAction, setSelectedTicketPin: (pinId: string) => SetSelectedTicketPinAction, setPinToDrop: (pinToDrop: string) => SetPinToDropAction, + setFilters: (filters: CardFilter[]) => SetFiltersAction, upsertFilter: (filter: CardFilter) => UpsertFilterAction, deleteFilter: (filter: CardFilter) => DeleteFilterAction, resetFilters: () => ResetFiltersAction, diff --git a/frontend/src/v5/ui/controls/chip/chip.types.tsx b/frontend/src/v5/ui/controls/chip/chip.types.tsx index c81bab7480b..c44a9fe9b3f 100644 --- a/frontend/src/v5/ui/controls/chip/chip.types.tsx +++ b/frontend/src/v5/ui/controls/chip/chip.types.tsx @@ -176,7 +176,7 @@ export const STATUS_TYPE_MAP = { }, }; -enum TicketStatusDefaultValues { +export enum TicketStatusDefaultValues { OPEN = 'Open', IN_PROGRESS = 'In Progress', FOR_APPROVAL = 'For Approval', diff --git a/frontend/src/v5/ui/routes/viewer/defaultTicketFiltersSetter/defaultTicketFiltersSetter.component.tsx b/frontend/src/v5/ui/routes/viewer/defaultTicketFiltersSetter/defaultTicketFiltersSetter.component.tsx new file mode 100644 index 00000000000..60dd28ba1fc --- /dev/null +++ b/frontend/src/v5/ui/routes/viewer/defaultTicketFiltersSetter/defaultTicketFiltersSetter.component.tsx @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2025 3D Repo Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { TicketsCardActionsDispatchers, ViewerGuiActionsDispatchers } from '@/v5/services/actionsDispatchers'; +import { TicketsHooksSelectors } from '@/v5/services/selectorsHooks'; +import { isEmpty, uniq } from 'lodash'; +import { useParams } from 'react-router-dom'; +import { useEffect } from 'react'; +import { ViewerParams } from '../../routes.constants'; +import { Transformers, useSearchParam } from '../../useSearchParam'; +import { VIEWER_PANELS } from '@/v4/constants/viewerGui'; +import { CardFilter } from '@components/viewer/cards/cardFilters/cardFilters.types'; +import { StatusValue } from '@/v5/store/tickets/tickets.types'; +import { TicketStatusDefaultValues, TicketStatusTypes, TreatmentStatuses } from '@controls/chip/chip.types'; +import { selectStatusConfigByTemplateId } from '@/v5/store/tickets/tickets.selectors'; +import { getState } from '@/v5/helpers/redux.helpers'; + +const TICKET_CODE_REGEX = /^[a-zA-Z]{3}:\d+$/; +export const DefaultTicketFiltersSetter = () => { + const { containerOrFederation } = useParams(); + const [ticketSearchParam, setTicketSearchParam] = useSearchParam('ticketSearch', Transformers.STRING_ARRAY); + + const tickets = TicketsHooksSelectors.selectTickets(containerOrFederation); + const templates = TicketsHooksSelectors.selectTemplates(containerOrFederation); + const hasTicketData = !isEmpty(tickets) && !isEmpty(templates); + + const getTicketFiltersFromURL = (values): CardFilter[] => [{ + module: '', + property: 'Ticket ID', + type: 'ticketId', + filter: { + operator: 'eq', + values, + }, + }]; + + const getNonCompletedTicketFiltersByStatus = (): CardFilter => { + const isCompletedValue = ({ type }: StatusValue) => [TicketStatusTypes.DONE, TicketStatusTypes.VOID].includes(type); + const getValuesByTemplate = ({ _id }) => selectStatusConfigByTemplateId(getState(), containerOrFederation, _id).values; + + const completedValueNames = templates + .flatMap(getValuesByTemplate) + .filter(isCompletedValue) + .map((v) => v.name); + + const values = uniq([ + TicketStatusDefaultValues.CLOSED, + TicketStatusDefaultValues.VOID, + ...completedValueNames, + ]); + + return { + module: '', + property: 'Status', + type: 'oneOf', + filter: { + operator: 'neq', + values, + }, + }; + }; + const getNonCompletedTicketFiltersBySafetibase = (): CardFilter => ({ + module: 'safetibase', + property: 'Treatment Status', + type: 'oneOf', + filter: { + operator: 'neq', + values: [ + TreatmentStatuses.REJECTED, + TreatmentStatuses.VOID, + ], + }, + }); + + const getNonCompletedTicketFilters = (): CardFilter[] => { + let filters = [getNonCompletedTicketFiltersByStatus()]; + const hasSafetibase = templates.some((t) => t?.modules?.some((module) => module.type === 'safetibase')); + if (hasSafetibase) { + filters.push(getNonCompletedTicketFiltersBySafetibase()); + } + return filters; + }; + + useEffect(() => { + if (hasTicketData) { + const ticketCodes = ticketSearchParam.filter((query) => TICKET_CODE_REGEX.test(query)).map((q) => q.toUpperCase()); + const filters: CardFilter[] = ticketCodes.length ? getTicketFiltersFromURL(ticketCodes) : getNonCompletedTicketFilters(); + TicketsCardActionsDispatchers.setFilters(filters); + ViewerGuiActionsDispatchers.setPanelVisibility(VIEWER_PANELS.TICKETS, true); + setTicketSearchParam(); + } + }, [hasTicketData]); + + return <>; +}; diff --git a/frontend/src/v5/ui/routes/viewer/handleTicketsCardSearchParams/handleTicketsCardSearchParams.component.tsx b/frontend/src/v5/ui/routes/viewer/handleTicketsCardSearchParams/handleTicketsCardSearchParams.component.tsx deleted file mode 100644 index 7c28b41968d..00000000000 --- a/frontend/src/v5/ui/routes/viewer/handleTicketsCardSearchParams/handleTicketsCardSearchParams.component.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (C) 2023 3D Repo Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { DialogsActionsDispatchers, TicketsCardActionsDispatchers, ViewerGuiActionsDispatchers } from '@/v5/services/actionsDispatchers'; -import { TicketsHooksSelectors } from '@/v5/services/selectorsHooks'; -import { isEmpty } from 'lodash'; -import { useParams } from 'react-router-dom'; -import { useEffect } from 'react'; -import { formatMessage } from '@/v5/services/intl'; -import { ViewerParams } from '../../routes.constants'; -import { Transformers, useSearchParam } from '../../useSearchParam'; -import { VIEWER_PANELS } from '@/v4/constants/viewerGui'; - -export const HandleTicketsCardSearchParams = () => { - const { containerOrFederation } = useParams(); - const [ticketId, setTicketId] = useSearchParam('ticketId'); - - const [ticketSearchParam, setTicketSearchParam] = useSearchParam('ticketSearch', Transformers.STRING_ARRAY); - const [ticketTemplatesParam, setTicketTemplatesParam] = useSearchParam('ticketTemplates', Transformers.STRING_ARRAY); - const [ticketCompletedParam, setTicketCompletedParam] = useSearchParam('ticketCompleted', Transformers.BOOLEAN); - - const tickets = TicketsHooksSelectors.selectTickets(containerOrFederation); - const templates = TicketsHooksSelectors.selectTemplates(containerOrFederation); - const hasTicketData = !isEmpty(tickets) && !isEmpty(templates); - - useEffect(() => { - if (ticketId && hasTicketData) { - if (!tickets.some(({ _id }) => _id === ticketId)) { - DialogsActionsDispatchers.open('warning', { - title: formatMessage({ id: 'openTicketFromUrl.invalidTicketId.title', defaultMessage: 'Ticket not found' }), - message: formatMessage({ id: 'openTicketFromUrl.invalidTicketId.message', defaultMessage: 'A ticket with this ID could not be found. Ensure that you have the correct URL' }), - }); - setTicketId(''); - return; - } - TicketsCardActionsDispatchers.openTicket(ticketId); - } - }, [hasTicketData]); - - useEffect(() => { - if (!ticketTemplatesParam.length && !ticketSearchParam.length && !ticketCompletedParam) return; - ViewerGuiActionsDispatchers.setPanelVisibility(VIEWER_PANELS.TICKETS, true); - // TODO - waiting for refactor of URL params or a decision to create an adapter here - // TODO - to use new filtering logic - // TicketsCardActionsDispatchers.setTemplateFilters(ticketTemplatesParam); - // if (ticketSearchParam.length) { - // TicketsCardActionsDispatchers.setQueryFilters(ticketSearchParam); - // } - // if (ticketCompletedParam) { - // TicketsCardActionsDispatchers.toggleCompleteFilter(); - // } - - setTicketSearchParam(); - setTicketCompletedParam(); - setTicketTemplatesParam(); - }, [ticketTemplatesParam, ticketSearchParam, ticketCompletedParam]); - - return <>; -}; diff --git a/frontend/src/v5/ui/routes/viewer/viewer.tsx b/frontend/src/v5/ui/routes/viewer/viewer.tsx index f48f9636c9a..ec9036961ec 100644 --- a/frontend/src/v5/ui/routes/viewer/viewer.tsx +++ b/frontend/src/v5/ui/routes/viewer/viewer.tsx @@ -24,7 +24,7 @@ import { VIEWER_EVENTS } from '@/v4/constants/viewer'; import { CheckLatestRevisionReadiness } from './checkLatestRevisionReadiness/checkLatestRevisionReadiness.container'; import { ViewerParams } from '../routes.constants'; import { InvalidContainerOverlay, InvalidFederationOverlay } from './invalidViewerOverlay'; -import { HandleTicketsCardSearchParams } from './handleTicketsCardSearchParams/handleTicketsCardSearchParams.component'; +import { DefaultTicketFiltersSetter } from './defaultTicketFiltersSetter/defaultTicketFiltersSetter.component'; import { SpinnerLoader } from '@controls/spinnerLoader'; import { CentredContainer } from '@controls/centredContainer'; import { TicketsCardViews } from './tickets/tickets.constants'; @@ -111,7 +111,7 @@ export const Viewer = () => { return ( <> - + diff --git a/frontend/test/tickets/card/ticketsCard.store.spec.ts b/frontend/test/tickets/card/ticketsCard.store.spec.ts index 376de89fb90..d7c21977ce9 100644 --- a/frontend/test/tickets/card/ticketsCard.store.spec.ts +++ b/frontend/test/tickets/card/ticketsCard.store.spec.ts @@ -78,6 +78,12 @@ describe('Tickets: store', () => { const updatedTicketTitleCardFilter: CardFilter = { ...ticketTitleCardFilter, filter: editedBaseFilter }; describe('existing filters', () => { + it('should set 2 filters', () => { + dispatch(TicketsCardActions.setFilters([ticketIdCardFilter, ticketTitleCardFilter])); + const filtersInStore = selectCardFilters(getState()); + expect(filtersInStore).toEqual([ticketIdCardFilter, ticketTitleCardFilter]); + }); + it('should add a filter', () => { dispatch(TicketsCardActions.upsertFilter(ticketTitleCardFilter)); const filtersInStore = selectCardFilters(getState()); From c55efdff9190147f7dc4c9bfaf24de6cf85962c4 Mon Sep 17 00:00:00 2001 From: Alessandro Amantini Date: Fri, 7 Feb 2025 10:02:27 +0000 Subject: [PATCH 2/3] ISSUE #5374 - avoid creating extra redux call --- .../src/v5/store/tickets/card/ticketsCard.redux.ts | 11 ----------- .../defaultTicketFiltersSetter.component.tsx | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/frontend/src/v5/store/tickets/card/ticketsCard.redux.ts b/frontend/src/v5/store/tickets/card/ticketsCard.redux.ts index 4cdcbbeb486..16d0fd68361 100644 --- a/frontend/src/v5/store/tickets/card/ticketsCard.redux.ts +++ b/frontend/src/v5/store/tickets/card/ticketsCard.redux.ts @@ -28,7 +28,6 @@ export const { Types: TicketsCardTypes, Creators: TicketsCardActions } = createA setSelectedTicket: ['ticketId'], setSelectedTemplate: ['templateId'], setSelectedTicketPin: ['pinId'], - setFilters: ['filters'], upsertFilter: ['filter'], deleteFilter: ['filter'], resetFilters: [], @@ -94,13 +93,6 @@ export const setPinToDrop = (state: ITicketsCardState, { pinToDrop }: SetPinToDr }; const getFilterKey = ({ module, property, type }: CardFilter): TicketFilterKey => `${module}.${property}.${type}`; -export const setFilters = (state: ITicketsCardState, { filters }: SetFiltersAction) => { - filters.forEach((filter) => { - const path = getFilterKey(filter); - state.filters[path] = filter.filter; - }); -}; - export const upsertFilter = (state: ITicketsCardState, { filter }: UpsertFilterAction) => { const path = getFilterKey(filter); state.filters[path] = filter.filter; @@ -152,7 +144,6 @@ export const ticketsCardReducer = createReducer(INITIAL_STATE, produceAll({ [TicketsCardTypes.SET_SELECTED_TEMPLATE]: setSelectedTemplate, [TicketsCardTypes.SET_SELECTED_TICKET_PIN]: setSelectedTicketPin, [TicketsCardTypes.SET_PIN_TO_DROP]: setPinToDrop, - [TicketsCardTypes.SET_FILTERS]: setFilters, [TicketsCardTypes.UPSERT_FILTER]: upsertFilter, [TicketsCardTypes.DELETE_FILTER]: deleteFilter, [TicketsCardTypes.RESET_FILTERS]: resetFilters, @@ -169,7 +160,6 @@ export type SetSelectedTicketAction = Action<'SET_SELECTED_TICKET'> & { ticketId export type SetSelectedTemplateAction = Action<'SET_SELECTED_TEMPLATE'> & { templateId: string }; export type SetSelectedTicketPinAction = Action<'SET_SELECTED_TICKET_PIN'> & { pinId: string }; export type SetPinToDropAction = Action<'SET_PIN_TO_DROP'> & { pinToDrop: string }; -export type SetFiltersAction = Action<'SET_FILTERS'> & { filters: CardFilter[] }; export type UpsertFilterAction = Action<'UPSERT_FILTER'> & { filter: CardFilter }; export type DeleteFilterAction = Action<'DELETE_FILTER'> & { filter: CardFilter }; export type ResetFiltersAction = Action<'RESET_FILTERS'>; @@ -189,7 +179,6 @@ export interface ITicketsCardActionCreators { setSelectedTemplate: (templateId: string) => SetSelectedTemplateAction, setSelectedTicketPin: (pinId: string) => SetSelectedTicketPinAction, setPinToDrop: (pinToDrop: string) => SetPinToDropAction, - setFilters: (filters: CardFilter[]) => SetFiltersAction, upsertFilter: (filter: CardFilter) => UpsertFilterAction, deleteFilter: (filter: CardFilter) => DeleteFilterAction, resetFilters: () => ResetFiltersAction, diff --git a/frontend/src/v5/ui/routes/viewer/defaultTicketFiltersSetter/defaultTicketFiltersSetter.component.tsx b/frontend/src/v5/ui/routes/viewer/defaultTicketFiltersSetter/defaultTicketFiltersSetter.component.tsx index 60dd28ba1fc..e554167bc0e 100644 --- a/frontend/src/v5/ui/routes/viewer/defaultTicketFiltersSetter/defaultTicketFiltersSetter.component.tsx +++ b/frontend/src/v5/ui/routes/viewer/defaultTicketFiltersSetter/defaultTicketFiltersSetter.component.tsx @@ -99,7 +99,7 @@ export const DefaultTicketFiltersSetter = () => { if (hasTicketData) { const ticketCodes = ticketSearchParam.filter((query) => TICKET_CODE_REGEX.test(query)).map((q) => q.toUpperCase()); const filters: CardFilter[] = ticketCodes.length ? getTicketFiltersFromURL(ticketCodes) : getNonCompletedTicketFilters(); - TicketsCardActionsDispatchers.setFilters(filters); + filters.forEach(TicketsCardActionsDispatchers.upsertFilter); ViewerGuiActionsDispatchers.setPanelVisibility(VIEWER_PANELS.TICKETS, true); setTicketSearchParam(); } From fec87dc57b054eddc717001a0b610d0185e8d1e2 Mon Sep 17 00:00:00 2001 From: Alessandro Amantini Date: Fri, 7 Feb 2025 10:11:30 +0000 Subject: [PATCH 3/3] ISSUE #5374 - fix tests --- frontend/test/tickets/card/ticketsCard.store.spec.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/frontend/test/tickets/card/ticketsCard.store.spec.ts b/frontend/test/tickets/card/ticketsCard.store.spec.ts index d7c21977ce9..376de89fb90 100644 --- a/frontend/test/tickets/card/ticketsCard.store.spec.ts +++ b/frontend/test/tickets/card/ticketsCard.store.spec.ts @@ -78,12 +78,6 @@ describe('Tickets: store', () => { const updatedTicketTitleCardFilter: CardFilter = { ...ticketTitleCardFilter, filter: editedBaseFilter }; describe('existing filters', () => { - it('should set 2 filters', () => { - dispatch(TicketsCardActions.setFilters([ticketIdCardFilter, ticketTitleCardFilter])); - const filtersInStore = selectCardFilters(getState()); - expect(filtersInStore).toEqual([ticketIdCardFilter, ticketTitleCardFilter]); - }); - it('should add a filter', () => { dispatch(TicketsCardActions.upsertFilter(ticketTitleCardFilter)); const filtersInStore = selectCardFilters(getState());