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..e554167bc0e --- /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(); + filters.forEach(TicketsCardActionsDispatchers.upsertFilter); + 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 ( <> - +