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