diff --git a/frontend/src/v5/store/jobs/jobs.types.ts b/frontend/src/v5/store/jobs/jobs.types.ts index 205657fe4b..1d596517c1 100644 --- a/frontend/src/v5/store/jobs/jobs.types.ts +++ b/frontend/src/v5/store/jobs/jobs.types.ts @@ -15,8 +15,12 @@ * along with this program. If not, see . */ +import { IUser } from '../users/users.redux'; + export type IJob = { _id: string; color: string; isViewer?: boolean; }; + +export type IJobOrUserList = Partial[]; \ No newline at end of file diff --git a/frontend/src/v5/store/tickets/card/ticketsCard.selectors.ts b/frontend/src/v5/store/tickets/card/ticketsCard.selectors.ts index 972bf1be27..9f0e8d3b4f 100644 --- a/frontend/src/v5/store/tickets/card/ticketsCard.selectors.ts +++ b/frontend/src/v5/store/tickets/card/ticketsCard.selectors.ts @@ -23,8 +23,11 @@ import { ITicketsCardState } from './ticketsCard.redux'; import { DEFAULT_PIN, getPinColorHex, formatPin, getTicketPins } from '@/v5/ui/routes/viewer/tickets/ticketsForm/properties/coordsProperty/coordsProperty.helpers'; import { IPin } from '@/v4/services/viewer/viewer'; import { selectSelectedDate } from '@/v4/modules/sequences'; -import { uniq } from 'lodash'; -import { toTicketCardFilter, templatesToFilters } from '@components/viewer/cards/cardFilters/filtersSelection/tickets/ticketFilters.helpers'; +import { sortBy, uniq, sortedUniqBy } from 'lodash'; +import { toTicketCardFilter, templatesToFilters, getFiltersFromJobsAndUsers } from '@components/viewer/cards/cardFilters/filtersSelection/tickets/ticketFilters.helpers'; +import { selectFederationById, selectFederationJobs, selectFederationUsers } from '../../federations/federations.selectors'; +import { selectContainerJobs, selectContainerUsers } from '../../containers/containers.selectors'; +import { IJobOrUserList } from '../../jobs/jobs.types'; const selectTicketsCardDomain = (state): ITicketsCardState => state.ticketsCard || {}; @@ -139,20 +142,28 @@ export const selectFilteredTickets = createSelector( }, ); -const selectTemplatesFilters = createSelector( +export const selectTemplatesWithTickets = createSelector( + selectCurrentTemplates, + selectCurrentTickets, + (templates, tickets) => { + const idsOfTemplatesWithAtLeastOneTicket = uniq(tickets.map((t) => t.type)); + return templates.filter((t) => idsOfTemplatesWithAtLeastOneTicket.includes(t._id)); + }, +); + +export const selectTemplatesWithFilteredTickets = createSelector( selectCurrentTemplates, selectFilteredTickets, (templates, tickets) => { const idsOfTemplatesWithAtLeastOneTicket = uniq(tickets.map((t) => t.type)); - const templatesWithAtLeastOneTicket = templates.filter((t) => idsOfTemplatesWithAtLeastOneTicket.includes(t._id)); - return templatesToFilters(templatesWithAtLeastOneTicket); + return templates.filter((t) => idsOfTemplatesWithAtLeastOneTicket.includes(t._id)); }, ); export const selectAvailableTemplatesFilters = createSelector( selectFilters, - selectTemplatesFilters, - (usedFilters, allFilters) => allFilters.filter(({ module, property, type }) => !usedFilters[`${module}.${property}.${type}`]), + selectTemplatesWithFilteredTickets, + (usedFilters, allFilters) => templatesToFilters(allFilters).filter(({ module, property, type }) => !usedFilters[`${module}.${property}.${type}`]), ); export const selectIsShowingPins = createSelector( @@ -207,24 +218,44 @@ export const selectNewTicketPins = createSelector( selectSelectedTicketPinId, getTicketPins, ); +const selectJobsAndUsersByModelId = createSelector( + selectFederationById, + selectFederationJobs, + selectContainerJobs, + selectFederationUsers, + selectContainerUsers, + (fed, fedJobs, contJobs, fedUsers, contUsers) => { + const isFed = !!fed; + const jobs = isFed ? fedJobs : contJobs; + const users = isFed ? fedUsers : contUsers; + return [...jobs, ...users] as IJobOrUserList; + }, +); export const selectPropertyOptions = createSelector( - selectTemplates, + selectTemplatesWithTickets, selectRiskCategories, + selectJobsAndUsersByModelId, (state, modelId, module) => module, (state, modelId, module, property) => property, - (templates, riskCategories, module, property) => { + (templates, riskCategories, jobsAndUsers, module, property) => { const allValues = []; + if (!module && property === 'Owner') return getFiltersFromJobsAndUsers(jobsAndUsers.filter((ju) => !!ju.firstName)); templates.forEach((template) => { const matchingModule = module ? template.modules.find((mod) => (mod.name || mod.type) === module)?.properties : template.properties; const matchingProperty = matchingModule?.find(({ name, type: t }) => (name === property) && (['manyOf', 'oneOf'].includes(t))); if (!matchingProperty) return; - if (matchingProperty.values === 'riskCategories') { - allValues.push(...riskCategories); - return; + switch (matchingProperty.values) { + case 'riskCategories': + allValues.push(...riskCategories.map((value) => ({ value, type: 'riskCategories' }))); + break; + case 'jobsAndUsers': + allValues.push(...getFiltersFromJobsAndUsers(jobsAndUsers)); + break; + default: + allValues.push(...matchingProperty.values.map((value) => ({ value, type: 'default' }))); } - allValues.push(...matchingProperty.values); }); - return uniq(allValues); + return sortedUniqBy(sortBy(allValues, 'value'), 'value'); }, ); diff --git a/frontend/src/v5/store/tickets/tickets.selectors.ts b/frontend/src/v5/store/tickets/tickets.selectors.ts index 77f4a94d82..da48d32cde 100644 --- a/frontend/src/v5/store/tickets/tickets.selectors.ts +++ b/frontend/src/v5/store/tickets/tickets.selectors.ts @@ -53,11 +53,6 @@ export const selectTemplates = createSelector( (state, modelId) => state.templatesByModelId[modelId] || [], ); -export const selectTemplatesNames = createSelector( - selectTemplates, - (templates) => templates.map(({ name }) => name), -); - export const selectTemplateById = createSelector( selectTicketsDomain, selectTemplates, diff --git a/frontend/src/v5/ui/components/viewer/cards/cardFilters/cardFilters.helpers.ts b/frontend/src/v5/ui/components/viewer/cards/cardFilters/cardFilters.helpers.ts index 558be7ef47..2468ca3d3f 100644 --- a/frontend/src/v5/ui/components/viewer/cards/cardFilters/cardFilters.helpers.ts +++ b/frontend/src/v5/ui/components/viewer/cards/cardFilters/cardFilters.helpers.ts @@ -73,7 +73,7 @@ const DATE_FILTER_OPERATOR_LABEL: Record = { export const isDateType = (type: CardFilterType) => ['date', 'pastDate', 'sequencing'].includes(type); export const isTextType = (type: CardFilterType) => ['ticketCode', 'title', 'text', 'longText'].includes(type); -export const isSelectType = (type: CardFilterType) => ['template', 'oneOf', 'manyOf'].includes(type); +export const isSelectType = (type: CardFilterType) => ['template', 'oneOf', 'manyOf', 'owner'].includes(type); export const getFilterOperatorLabels = (type: CardFilterType) => isDateType(type) ? DATE_FILTER_OPERATOR_LABEL : FILTER_OPERATOR_LABEL; @@ -90,7 +90,7 @@ export const getValidOperators = (type: CardFilterType): CardFilterOperator[] => if (isDateType(type)) return ['ex', 'nex', 'eq', 'neq', 'gte', 'lte', 'rng', 'nrng']; if (type === 'boolean') return ['eq', 'ex', 'nex']; if (isSelectType(type)) { - if (type === 'template') return ['is', 'nis']; + if (['template', 'owner'].includes(type)) return ['is', 'nis']; return ['ex', 'nex', 'is', 'nis']; } return Object.keys(FILTER_OPERATOR_LABEL) as CardFilterOperator[]; diff --git a/frontend/src/v5/ui/components/viewer/cards/cardFilters/cardFilters.types.ts b/frontend/src/v5/ui/components/viewer/cards/cardFilters/cardFilters.types.ts index 51745a5d73..353445f02b 100644 --- a/frontend/src/v5/ui/components/viewer/cards/cardFilters/cardFilters.types.ts +++ b/frontend/src/v5/ui/components/viewer/cards/cardFilters/cardFilters.types.ts @@ -16,10 +16,10 @@ */ export type CardFilterOperator = 'ex' | 'nex' | 'is' | 'nis' | 'eq' | 'neq' | 'ss' | 'nss' | 'rng' | 'nrng' | 'gt' | 'gte' | 'lt' | 'lte'; -export type CardFilterType = 'text' | 'longText' | 'date' | 'sequencing' | 'pastDate' | 'oneOf' | 'manyOf' | 'boolean' | 'number' | 'title' | 'ticketCode' | 'template'; +export type CardFilterType = 'text' | 'longText' | 'date' | 'sequencing' | 'pastDate' | 'oneOf' | 'manyOf' | 'boolean' | 'number' | 'title' | 'ticketCode' | 'template' | 'owner'; type ValueType = string | number | Date; export type CardFilterValue = ValueType | ValueType[]; -export type BaseFilter = { operator: CardFilterOperator, values: CardFilterValue[] }; +export type BaseFilter = { operator: CardFilterOperator, values: CardFilterValue[], displayValues?: string }; export type CardFilter = { property: string, diff --git a/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterChip/filterChip.component.tsx b/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterChip/filterChip.component.tsx index 825ba264a9..1ec04c5203 100644 --- a/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterChip/filterChip.component.tsx +++ b/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterChip/filterChip.component.tsx @@ -17,27 +17,10 @@ import CloseIcon from '@assets/icons/outlined/close-outlined.svg'; import { ChipContainer, DeleteButton, TextWrapper, OperatorIconContainer, DisplayValue, Property } from './filterChip.styles'; -import { FILTER_OPERATOR_ICON, getFilterOperatorLabels, isDateType, isRangeOperator } from '../cardFilters.helpers'; +import { FILTER_OPERATOR_ICON, getFilterOperatorLabels } from '../cardFilters.helpers'; import { Tooltip } from '@mui/material'; import { FormattedMessage } from 'react-intl'; -import { CardFilterType, BaseFilter, CardFilterOperator, CardFilterValue } from '../cardFilters.types'; -import { formatSimpleDate } from '@/v5/helpers/intl.helper'; -import { formatMessage } from '@/v5/services/intl'; -import { isBoolean } from 'lodash'; -import { FALSE_LABEL, TRUE_LABEL } from '@controls/inputs/booleanSelect/booleanSelect.component'; - -const valueToDisplayDate = (value) => formatSimpleDate(new Date(value)); -const formatDateRange = ([from, to]) => formatMessage( - { defaultMessage: '{from} to {to}', id: 'cardFilter.dateRange.join' }, - { from: valueToDisplayDate(from), to: valueToDisplayDate(to) }, -); - -const getDisplayValue = (values: CardFilterValue[], operator: CardFilterOperator, type: CardFilterType) => { - const isRange = isRangeOperator(operator); - if (isDateType(type)) return values.map(isRange ? formatDateRange : valueToDisplayDate); - if (type === 'boolean' && isBoolean(values[0])) return values[0] ? TRUE_LABEL : FALSE_LABEL; - return (isRange ? values.map(([a, b]: any) => `[${a}, ${b}]`) : values).join(', ') ?? ''; -}; +import { CardFilterType, BaseFilter } from '../cardFilters.types'; type FilterChipProps = { property: string; @@ -47,10 +30,9 @@ type FilterChipProps = { onDelete: () => void; }; export const FilterChip = ({ property, onDelete, selected, type, filter }: FilterChipProps) => { - const { operator, values } = filter; + const { operator, values, displayValues = values.join(', ') } = filter; const OperatorIcon = FILTER_OPERATOR_ICON[operator]; const hasMultipleValues = values.length > 1; - const displayValue = getDisplayValue(values, operator, type); const labels = getFilterOperatorLabels(type); const handleDelete = (e) => { @@ -61,7 +43,7 @@ export const FilterChip = ({ property, onDelete, selected, type, filter }: Filte return ( - + {property} @@ -73,7 +55,7 @@ export const FilterChip = ({ property, onDelete, selected, type, filter }: Filte )} {!hasMultipleValues && !!values?.length && ( - {displayValue} + {displayValues} )} diff --git a/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterForm/filterForm.component.tsx b/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterForm/filterForm.component.tsx index 1661ceddbc..b7f5b36e46 100644 --- a/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterForm/filterForm.component.tsx +++ b/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterForm/filterForm.component.tsx @@ -17,20 +17,24 @@ import { FormattedMessage } from 'react-intl'; import { CardFilterOperator, CardFilterValue, CardFilterType, BaseFilter, CardFilter } from '../cardFilters.types'; -import { getFilterFormTitle } from '../cardFilters.helpers'; +import { getFilterFormTitle, getValidOperators, isDateType, isRangeOperator } from '../cardFilters.helpers'; import { Container, ButtonsContainer, Button, TitleContainer } from './filterForm.styles'; import { FormProvider, useForm } from 'react-hook-form'; -import { isEmpty } from 'lodash'; +import { intersection, isBoolean, isEmpty } from 'lodash'; import { ActionMenuItem } from '@controls/actionMenu'; import { FilterFormValues } from './filterFormValues/filterFormValues.component'; import { mapArrayToFormArray, mapFormArrayToArray } from '@/v5/helpers/form.helper'; import { yupResolver } from '@hookform/resolvers/yup'; import { FilterSchema } from '@/v5/validation/ticketSchemes/validators'; import { FilterFormOperators } from './filterFormValues/operators/filterFormOperators.component'; +import { getOptionFromValue } from '../filtersSelection/tickets/ticketFilters.helpers'; +import { formatSimpleDate } from '@/v5/helpers/intl.helper'; +import { formatMessage } from '@/v5/services/intl'; +import { TRUE_LABEL, FALSE_LABEL } from '@controls/inputs/booleanSelect/booleanSelect.component'; -const DEFAULT_OPERATOR = 'is'; +const DEFAULT_OPERATORS: CardFilterOperator[] = ['is', 'eq']; const DEFAULT_VALUES = ['']; -type FormType = { values: { value: CardFilterValue }[], operator: CardFilterOperator }; +type FormType = { values: { value: CardFilterValue, displayValue?: string }[], operator: CardFilterOperator }; type FilterFormProps = { module: string, property: string, @@ -39,9 +43,16 @@ type FilterFormProps = { onSubmit: (newFilter: CardFilter) => void, onCancel: () => void, }; + +const valueToDisplayDate = (value) => formatSimpleDate(new Date(value)); +const formatDateRange = ([from, to]) => formatMessage( + { defaultMessage: '{from} to {to}', id: 'cardFilter.dateRange.join' }, + { from: valueToDisplayDate(from), to: valueToDisplayDate(to) }, +); + export const FilterForm = ({ module, property, type, filter, onSubmit, onCancel }: FilterFormProps) => { - const defaultValues = { - operator: filter?.operator || DEFAULT_OPERATOR, + const defaultValues: FormType = { + operator: filter?.operator || intersection(getValidOperators(type), DEFAULT_OPERATORS)[0], values: mapArrayToFormArray(filter?.values || DEFAULT_VALUES), }; @@ -50,8 +61,14 @@ export const FilterForm = ({ module, property, type, filter, onSubmit, onCancel mode: 'onChange', resolver: yupResolver(FilterSchema), context: { type }, + shouldUnregister: true, }); - const { formState: { isValid, dirtyFields }, reset } = formData; + const { formState: { isValid, dirtyFields }, reset, getValues } = formData; + + const operatorValue = getValues('operator'); + if (!getValidOperators(type).includes(operatorValue)) { + reset(defaultValues); + } const isUpdatingFilter = !!filter; const canSubmit = isValid && !isEmpty(dirtyFields); @@ -59,7 +76,18 @@ export const FilterForm = ({ module, property, type, filter, onSubmit, onCancel const handleSubmit = formData.handleSubmit((body: FormType) => { const newValues = mapFormArrayToArray(body.values) .filter((x) => ![undefined, ''].includes(x as any)); - onSubmit({ module, property, type, filter: { operator: body.operator, values: newValues } }); + const isRange = isRangeOperator(body.operator); + const displayValues = newValues.map((newVal) => { + const option = getOptionFromValue(newVal, body.values); + if (isDateType(type)) return (isRange ? formatDateRange(newVal) : valueToDisplayDate(newVal)); + if (type === 'boolean' && isBoolean(newValues[0])) return newValues[0] ? TRUE_LABEL : FALSE_LABEL; + if (isRange) { + const [a, b] = newVal; + return `[${a}, ${b}]`; + } + return option.displayValue ?? newVal; + }).join(', '); + onSubmit({ module, property, type, filter: { operator: body.operator, values: newValues, displayValues } }); }); const handleCancel = () => { diff --git a/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterForm/filterForm.styles.ts b/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterForm/filterForm.styles.ts index 53871194f8..c614dd6f42 100644 --- a/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterForm/filterForm.styles.ts +++ b/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterForm/filterForm.styles.ts @@ -31,6 +31,7 @@ export const Container = styled.div` display: flex; flex-direction: column; gap: 10px; + max-width: 365px; `; export const TitleContainer = styled.div` diff --git a/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterForm/filterFormValues/filterFormValues.component.tsx b/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterForm/filterFormValues/filterFormValues.component.tsx index a7d14f389e..709fb48fa4 100644 --- a/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterForm/filterFormValues/filterFormValues.component.tsx +++ b/frontend/src/v5/ui/components/viewer/cards/cardFilters/filterForm/filterFormValues/filterFormValues.component.tsx @@ -18,20 +18,21 @@ import { useFieldArray, useFormContext } from 'react-hook-form'; import { getOperatorMaxFieldsAllowed } from '../filterForm.helpers'; import { isRangeOperator, isTextType, isSelectType, isDateType } from '../../cardFilters.helpers'; -import { FormBooleanSelect, FormMultiSelect, FormDateTime, FormNumberField, FormTextField } from '@controls/inputs/formInputs.component'; +import { FormBooleanSelect, FormMultiSelect, FormDateTime, FormNumberField, FormTextField, FormJobsAndUsersSelect } from '@controls/inputs/formInputs.component'; import { ArrayFieldContainer } from '@controls/inputs/arrayFieldContainer/arrayFieldContainer.component'; import { useEffect } from 'react'; import { compact, isArray, isEmpty } from 'lodash'; import { CardFilterType } from '../../cardFilters.types'; -import { TicketsCardHooksSelectors, TicketsHooksSelectors } from '@/v5/services/selectorsHooks'; +import { TicketsCardHooksSelectors } from '@/v5/services/selectorsHooks'; import { useParams } from 'react-router-dom'; import { ViewerParams } from '@/v5/ui/routes/routes.constants'; import { MultiSelectMenuItem } from '@controls/inputs/multiSelect/multiSelectMenuItem/multiSelectMenuItem.component'; import { DateRangeInput } from './rangeInput/dateRangeInput.component'; import { NumberRangeInput } from './rangeInput/numberRangeInput.component'; -import { mapArrayToFormArray, mapFormArrayToArray } from '@/v5/helpers/form.helper'; +import { mapFormArrayToArray } from '@/v5/helpers/form.helper'; +import { getOptionFromValue, getFilterFromEvent } from '../../filtersSelection/tickets/ticketFilters.helpers'; -type FilterFolrmValuesType = { +type FilterFormValuesProps = { module: string, property: string, type: CardFilterType, @@ -44,7 +45,7 @@ const getInputField = (type: CardFilterType) => { }; const name = 'values'; -export const FilterFormValues = ({ module, property, type }: FilterFolrmValuesType) => { +export const FilterFormValues = ({ module, property, type }: FilterFormValuesProps) => { const { containerOrFederation } = useParams(); const { control, watch, formState: { errors, dirtyFields } } = useFormContext(); const { fields, append, remove } = useFieldArray({ @@ -58,7 +59,7 @@ export const FilterFormValues = ({ module, property, type }: FilterFolrmValuesTy const isRangeOp = isRangeOperator(operator); const emptyValue = { value: (isRangeOp ? ['', ''] : '') }; const selectOptions = type === 'template' ? - TicketsHooksSelectors.selectTemplatesNames(containerOrFederation) + TicketsCardHooksSelectors.selectTemplatesWithTickets().map(({ code: value, name: displayValue }) => ({ value, displayValue, type: 'template' })) : TicketsCardHooksSelectors.selectPropertyOptions(containerOrFederation, module, property); useEffect(() => { @@ -122,15 +123,32 @@ export const FilterFormValues = ({ module, property, type }: FilterFolrmValuesTy ); } + if (isSelectType(type)) { + const allJobsAndUsers = selectOptions.every(({ type: t }) => t === 'jobsAndUsers'); + if (allJobsAndUsers) return ( + compact(mapFormArrayToArray(v))} + transformOutputValue={(e) => getFilterFromEvent(e, selectOptions)} + formError={error?.[0]} + /> + ); return ( getFilterFromEvent(e, selectOptions)} + renderValue={(values: string[]) => values.map((value) => getOptionFromValue(value, selectOptions)?.displayValue ?? value).join(', ')} formError={error?.[0]} - transformValueIn={mapFormArrayToArray} - transformChangeEvent={(e) => mapArrayToFormArray(compact(e.target.value))} > - {(selectOptions || []).map((val) => {val})} + {selectOptions.map( + (option) => {option.displayValue ?? option.value}, + )} ); } @@ -143,4 +161,4 @@ export const FilterFormValues = ({ module, property, type }: FilterFolrmValuesTy ); -}; \ No newline at end of file +}; diff --git a/frontend/src/v5/ui/components/viewer/cards/cardFilters/filtersSelection/tickets/ticketFilters.helpers.ts b/frontend/src/v5/ui/components/viewer/cards/cardFilters/filtersSelection/tickets/ticketFilters.helpers.ts index ee32a8aace..e1f5eadaec 100644 --- a/frontend/src/v5/ui/components/viewer/cards/cardFilters/filtersSelection/tickets/ticketFilters.helpers.ts +++ b/frontend/src/v5/ui/components/viewer/cards/cardFilters/filtersSelection/tickets/ticketFilters.helpers.ts @@ -23,7 +23,7 @@ import NumberIcon from '@assets/icons/filters/number.svg'; import TemplateIcon from '@assets/icons/filters/template.svg'; import TextIcon from '@assets/icons/filters/text.svg'; import CalendarIcon from '@assets/icons/outlined/calendar-outlined.svg'; -import { isString, sortBy, uniqBy } from 'lodash'; +import { isString, sortBy, uniqBy, compact } from 'lodash'; import { CardFilterType, BaseFilter, CardFilter } from '../../cardFilters.types'; export const TYPE_TO_ICON: Record = { @@ -37,6 +37,7 @@ export const TYPE_TO_ICON: Record = { 'sequencing': CalendarIcon, 'oneOf': ListIcon, 'manyOf': ListIcon, + 'owner': ListIcon, 'boolean': BooleanIcon, 'number': NumberIcon, }; @@ -45,10 +46,11 @@ const DEFAULT_FILTERS: CardFilter[] = [ { module: '', type: 'title', property: formatMessage({ defaultMessage: 'Ticket title', id: 'viewer.card.filters.element.title' }) }, { module: '', type: 'ticketCode', property: formatMessage({ defaultMessage: 'Ticket ID', id: 'viewer.card.filters.element.ticketCode' }) }, { module: '', type: 'template', property: formatMessage({ defaultMessage: 'Ticket template', id: 'viewer.card.filters.element.template' }) }, + { module: '', type: 'owner', property: formatMessage({ defaultMessage: 'Owner', id: 'viewer.card.filters.element.owner' }) }, ]; const propertiesToValidFilters = (properties: { name: string, type: string }[], module: string = ''): CardFilter[] => properties - .filter(({ type }) => Object.keys(TYPE_TO_ICON).includes(type)) + .filter(({ name, type }) => name !== 'Owner' && Object.keys(TYPE_TO_ICON).includes(type)) .map(({ name, type }) => ({ module, property: name, @@ -83,6 +85,21 @@ export const toTicketCardFilter = (filters: Record): CardFil })) ); +export const getOptionFromValue = (value, options) => options.find(({ value: optionValue }) => value === optionValue); +export const getFilterFromEvent = (event, options) => compact(event.target.value).map((value) => { + const option = getOptionFromValue(value, options); + return { value, displayValue: option?.displayValue ?? value }; +}); + +export const getFiltersFromJobsAndUsers = (jobsAndUsers) => jobsAndUsers.map((ju) => { + const isUser = !!ju.firstName; + return ({ + value: isUser ? ju.user : ju._id, + displayValue: isUser ? `${ju?.firstName} ${ju?.lastName}` : null, + type: 'jobsAndUsers', + }); +}); + const wrapWith = (text, wrappingChar) => wrappingChar + text + wrappingChar; // This code, copied from MDN https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986 // is due to `encodeURIComponent` not encoding all the chars diff --git a/frontend/src/v5/ui/controls/assigneesSelect/assigneesSelect.component.tsx b/frontend/src/v5/ui/controls/assigneesSelect/assigneesSelect.component.tsx index e203b50158..086a297e40 100644 --- a/frontend/src/v5/ui/controls/assigneesSelect/assigneesSelect.component.tsx +++ b/frontend/src/v5/ui/controls/assigneesSelect/assigneesSelect.component.tsx @@ -33,6 +33,7 @@ export type AssigneesSelectProps = Pick & SelectProps & showEmptyText?: boolean; onBlur?: () => void; excludeViewers?: boolean; + excludeJobs?: boolean; }; export const AssigneesSelect = ({ @@ -45,6 +46,7 @@ export const AssigneesSelect = ({ onBlur, className, excludeViewers = false, + helperText, onChange, ...props }: AssigneesSelectProps) => { @@ -96,7 +98,6 @@ export const AssigneesSelect = ({ value={value} onClose={handleClose} onOpen={handleOpen} - disabled={disabled} multiple={multiple} isInvalid={(v) => invalidValues.includes(v)} onChange={handleChange} diff --git a/frontend/src/v5/ui/controls/assigneesSelect/assigneesSelectMenu/assigneesSelectMenu.component.tsx b/frontend/src/v5/ui/controls/assigneesSelect/assigneesSelectMenu/assigneesSelectMenu.component.tsx index 92d473f753..ae77821a51 100644 --- a/frontend/src/v5/ui/controls/assigneesSelect/assigneesSelectMenu/assigneesSelectMenu.component.tsx +++ b/frontend/src/v5/ui/controls/assigneesSelect/assigneesSelectMenu/assigneesSelectMenu.component.tsx @@ -41,6 +41,7 @@ const preventPropagation = (e) => { }; type AssigneesSelectMenuProps = SelectProps & { isInvalid: (val: string) => boolean; + excludeJobs?: boolean; }; export const AssigneesSelectMenu = ({ open, @@ -48,6 +49,7 @@ export const AssigneesSelectMenu = ({ onClick, multiple, isInvalid, + excludeJobs, ...props }: AssigneesSelectMenuProps) => { const { filteredItems } = useContext(SearchContext); @@ -89,11 +91,12 @@ export const AssigneesSelectMenu = ({ /> ))} {notFound.length > 0 && ()} - - - - - {jobs.length > 0 && jobs.map(({ _id }) => ( + {!excludeJobs && ( + + + + )} + {!excludeJobs && jobs.length > 0 && jobs.map(({ _id }) => ( ))} - {!jobs.length && ()} - - + {!excludeJobs && !jobs.length && ()} + {!excludeJobs && ( + + )} diff --git a/frontend/src/v5/ui/controls/inputs/formInputs.component.tsx b/frontend/src/v5/ui/controls/inputs/formInputs.component.tsx index 8063bd9227..8926f2241a 100644 --- a/frontend/src/v5/ui/controls/inputs/formInputs.component.tsx +++ b/frontend/src/v5/ui/controls/inputs/formInputs.component.tsx @@ -31,6 +31,7 @@ import { TextField, TextFieldProps } from './textField/textField.component'; import { DateTimePicker, DateTimePickerProps } from './datePicker/dateTimePicker.component'; import { BooleanSelect, BooleanSelectProps } from './booleanSelect/booleanSelect.component'; import { MultiSelect } from './multiSelect/multiSelect.component'; +import { JobsAndUsersProperty, JobsAndUsersPropertyProps } from '../../routes/viewer/tickets/ticketsForm/properties/jobsAndUsersProperty.component'; // text inputs export const FormNumberField = (props: InputControllerProps) => (); @@ -49,6 +50,7 @@ export const FormSelect = (props: InputControllerProps) => () => (); export const FormChipSelect = (props: InputControllerProps) => ( } {...props} />); export const FormSearchSelect = (props: InputControllerProps) => (); +export const FormJobsAndUsersSelect = (props: InputControllerProps) => (); export const FormBooleanSelect = (props: InputControllerProps) => (); // control inputs diff --git a/frontend/src/v5/ui/controls/inputs/inputController.component.tsx b/frontend/src/v5/ui/controls/inputs/inputController.component.tsx index 58670ad833..1537a8d583 100644 --- a/frontend/src/v5/ui/controls/inputs/inputController.component.tsx +++ b/frontend/src/v5/ui/controls/inputs/inputController.component.tsx @@ -36,8 +36,8 @@ export type InputControllerProps = T & FormInputProps & { defaultValue?: any, onChange?: (event) => void, onBlur?: () => void, - transformValueIn?: (val) => any, - transformChangeEvent?: (val) => any, + transformInputValue?: (val) => any, + transformOutputValue?: (val) => any, children?: any, }; @@ -55,8 +55,8 @@ export const InputController: InputControllerType = forwardRef(({ defaultValue, onChange, onBlur, - transformValueIn = (val) => val, - transformChangeEvent = (val) => val, + transformInputValue = (val) => val, + transformOutputValue = (val) => val, ...props }: Props, ref) => { const ctx = useFormContext(); @@ -67,27 +67,25 @@ export const InputController: InputControllerType = forwardRef(({ name={name} control={control} defaultValue={defaultValue} - render={({ field: { ref: fieldRef, ...field } }) => { - return ( + render={({ field: { ref: fieldRef, ...field } }) => ( // @ts-ignore - { - field.onChange(transformChangeEvent(event)); - onChange?.(transformChangeEvent(event)); - }} - onBlur={() => { - field.onBlur(); - onBlur?.(); - }} - inputRef={ref || fieldRef} - error={!!error} - helperText={error?.message} - /> - ); - }} + { + field.onChange(transformOutputValue(event)); + onChange?.(transformOutputValue(event)); + }} + onBlur={() => { + field.onBlur(); + onBlur?.(); + }} + inputRef={ref || fieldRef} + error={!!error} + helperText={error?.message} + /> + )} /> ); }); diff --git a/frontend/src/v5/ui/controls/inputs/multiSelect/multiSelect.component.tsx b/frontend/src/v5/ui/controls/inputs/multiSelect/multiSelect.component.tsx index d2d390d563..4688756241 100644 --- a/frontend/src/v5/ui/controls/inputs/multiSelect/multiSelect.component.tsx +++ b/frontend/src/v5/ui/controls/inputs/multiSelect/multiSelect.component.tsx @@ -17,16 +17,12 @@ import { SearchSelect } from '@controls/searchSelect/searchSelect.component'; import { SelectProps } from '../select/select.component'; -import { FormControl, FormHelperText } from '@mui/material'; -export const MultiSelect = ({ defaultValue = [], className, helperText, ...props }: SelectProps) => ( - - (val as any[]).join(', ')} - {...props} - multiple - /> - {helperText} - +export const MultiSelect = ({ defaultValue = [], className, ...props }: SelectProps) => ( + (val as any[]).join(', ')} + {...props} + multiple + /> ); 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 2e74bd0ebf..6ea0f6beda 100644 --- a/frontend/src/v5/ui/routes/viewer/defaultTicketFiltersSetter/defaultTicketFiltersSetter.component.tsx +++ b/frontend/src/v5/ui/routes/viewer/defaultTicketFiltersSetter/defaultTicketFiltersSetter.component.tsx @@ -16,7 +16,7 @@ */ import { TicketsCardActionsDispatchers, ViewerGuiActionsDispatchers } from '@/v5/services/actionsDispatchers'; -import { TicketsHooksSelectors } from '@/v5/services/selectorsHooks'; +import { TicketsCardHooksSelectors, TicketsHooksSelectors } from '@/v5/services/selectorsHooks'; import { isEmpty, uniq } from 'lodash'; import { useParams } from 'react-router-dom'; import { useEffect } from 'react'; @@ -35,7 +35,7 @@ export const DefaultTicketFiltersSetter = () => { const [ticketSearchParam, setTicketSearchParam] = useSearchParam('ticketSearch', Transformers.STRING_ARRAY); const tickets = TicketsHooksSelectors.selectTickets(containerOrFederation); - const templates = TicketsHooksSelectors.selectTemplates(containerOrFederation); + const templates = TicketsCardHooksSelectors.selectTemplatesWithTickets(); const hasTicketData = !isEmpty(tickets) && !isEmpty(templates); const getTicketFiltersFromURL = (values): CardFilter[] => [{ diff --git a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/jobsAndUsersProperty.component.tsx b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/jobsAndUsersProperty.component.tsx index 6c8cdf09d6..f3cd98ee5b 100644 --- a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/jobsAndUsersProperty.component.tsx +++ b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/jobsAndUsersProperty.component.tsx @@ -18,7 +18,7 @@ import { AssigneesSelect, AssigneesSelectProps } from '@controls/assigneesSelect import { FormInputProps } from '@controls/inputs/inputController.component'; import { FormControl, FormHelperText, InputLabel } from '@mui/material'; -type JobsAndUsersPropertyProps = FormInputProps & AssigneesSelectProps; +export type JobsAndUsersPropertyProps = FormInputProps & AssigneesSelectProps; export const JobsAndUsersProperty = ({ value, ...props }: JobsAndUsersPropertyProps) => ( {props.label} diff --git a/frontend/test/tickets/card/ticketsCard.store.spec.ts b/frontend/test/tickets/card/ticketsCard.store.spec.ts index ea5ac4ab8f..d1f2618fe6 100644 --- a/frontend/test/tickets/card/ticketsCard.store.spec.ts +++ b/frontend/test/tickets/card/ticketsCard.store.spec.ts @@ -64,7 +64,7 @@ describe('Tickets: store', () => { }); describe('filters', () => { - const [ticketTitleFilter, ticketIdFilter, templateIdFilter] = templatesToFilters([]); + const [ticketTitleFilter, ticketIdFilter, templateIdFilter, ownerFilter] = templatesToFilters([]); const baseFilter: BaseFilter = { operator: 'is', values: [], @@ -102,15 +102,15 @@ describe('Tickets: store', () => { describe('available template filters', () => { const getAvailableFilters = () => selectAvailableTemplatesFilters(getState()); it('all the default filters should be available originally', () => { - expect(getAvailableFilters()).toEqual([ticketTitleFilter, ticketIdFilter, templateIdFilter]); + expect(getAvailableFilters()).toEqual([ticketTitleFilter, ticketIdFilter, templateIdFilter, ownerFilter]); }) it('adding filters should make the unavailable', () => { // add first filter dispatch(TicketsCardActions.upsertFilter(ticketTitleCardFilter)); - expect(getAvailableFilters()).toEqual([ticketIdFilter, templateIdFilter]); + expect(getAvailableFilters()).toEqual([ticketIdFilter, templateIdFilter, ownerFilter]); // add second filter dispatch(TicketsCardActions.upsertFilter(ticketIdCardFilter)); - expect(getAvailableFilters()).toEqual([templateIdFilter]); + expect(getAvailableFilters()).toEqual([templateIdFilter, ownerFilter]); }) it('editing a filter shouldn\'t affect the available filters', () => { dispatch(TicketsCardActions.upsertFilter(ticketTitleCardFilter)); @@ -125,7 +125,7 @@ describe('Tickets: store', () => { dispatch(TicketsCardActions.upsertFilter(ticketIdCardFilter)); // delete filter dispatch(TicketsCardActions.deleteFilter(ticketTitleCardFilter)); - expect(getAvailableFilters()).toEqual([ticketTitleFilter, templateIdFilter]); + expect(getAvailableFilters()).toEqual([ticketTitleFilter, templateIdFilter, ownerFilter]); }) }); })