Skip to content

Commit

Permalink
feat: search ticket field by es
Browse files Browse the repository at this point in the history
  • Loading branch information
sdjdd committed Apr 10, 2024
1 parent a181b2b commit 341ab01
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 161 deletions.
217 changes: 84 additions & 133 deletions next/api/src/router/ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,6 @@ export const ticketFiltersSchema = yup.object({
privateTagValue: yup.string(),
language: yup.csv(yup.string().required()),

// fields
// TODO: use enum
fieldName: yup.string(),
fieldValue: yup.string(),

// pagination
page: yup.number().integer().min(1).default(1),
pageSize: yup.number().integer().min(0).max(100).default(10),
Expand Down Expand Up @@ -119,167 +114,123 @@ router.get(

const sortItems = sort.get(ctx);

const [finalQuery, count] = await (async () => {
if (params.fieldName && params.fieldValue) {
const ticketFieldQuery = TicketFieldValue.queryBuilder()
.where('values', '==', {
field: params.fieldName,
value: params.fieldValue,
})
.skip((params.page - 1) * params.pageSize)
.limit(params.pageSize)
.orderBy('createdAt', 'desc');

if (params.createdAtFrom) {
ticketFieldQuery.where('createdAt', '>=', params.createdAtFrom);
}
if (params.createdAtTo) {
ticketFieldQuery.where('createdAt', '<=', params.createdAtTo);
}

// we can't get the count in the second query, but we can in the first query
const [ticketFieldValues, count] = params.count
? await ticketFieldQuery.findAndCount({
useMasterKey: true,
})
: [await ticketFieldQuery.find({ useMasterKey: true }), undefined];

return [
Ticket.queryBuilder()
.where(
'objectId',
'in',
ticketFieldValues.map(({ ticketId }) => ticketId)
)
.orderBy('createdAt', 'desc'),
count,
];
} else {
const categoryIds = new Set(params.categoryId);
const rootId = params.product || params.rootCategoryId;

if (rootId) {
categoryIds.add(rootId);
const subCategories = await categoryService.getSubCategories(rootId);
subCategories.forEach((c) => categoryIds.add(c.id));
}

const query = Ticket.queryBuilder();

if (params.where) {
query.setRawCondition(params.where);
}
if (params.authorId) {
query.where('author', '==', User.ptr(params.authorId));
}
if (params.assigneeId) {
addPointersCondition(query, 'assignee', params.assigneeId, User);
}
if (params.groupId) {
addPointersCondition(query, 'group', params.groupId, Group);
}
if (params.reporterId) {
addPointersCondition(query, 'reporter', params.reporterId, User);
}
if (params.participantId) {
query.where('joinedCustomerServices.objectId', 'in', params.participantId);
}
if (categoryIds.size) {
query.where('category.objectId', 'in', Array.from(categoryIds));
}
if (params.status) {
query.where('status', 'in', params.status);
}
if (params['evaluation.star'] !== undefined) {
query.where('evaluation.star', '==', params['evaluation.star']);
}
if (params['evaluation.ts']) {
const [from, to] = params['evaluation.ts'];
if (from) {
query.where('evaluation.ts', '>=', from);
}
if (to) {
query.where('evaluation.ts', '<=', to);
}
}
if (params.createdAtFrom) {
query.where('createdAt', '>=', params.createdAtFrom);
}
if (params.createdAtTo) {
query.where('createdAt', '<=', params.createdAtTo);
}
if (params.tagKey) {
query.where('tags.key', '==', params.tagKey);
}
if (params.tagValue) {
query.where('tags.value', '==', params.tagValue);
}
if (params.privateTagKey) {
if (!(await currentUser.isCustomerService())) {
ctx.throw(403);
}
query.where('privateTags.key', '==', params.privateTagKey);
}
if (params.privateTagValue) {
if (!(await currentUser.isCustomerService())) {
ctx.throw(403);
}
query.where('privateTags.value', '==', params.privateTagValue);
}
const categoryIds = new Set(params.categoryId);
const rootId = params.product || params.rootCategoryId;

if (params.language) {
addInOrNotExistCondition(query, params.language, 'language');
}
if (rootId) {
categoryIds.add(rootId);
const subCategories = await categoryService.getSubCategories(rootId);
subCategories.forEach((c) => categoryIds.add(c.id));
}

query.skip((params.page - 1) * params.pageSize).limit(params.pageSize);
sortItems?.forEach(({ key, order }) => query.orderBy(key, order));
const query = Ticket.queryBuilder();

return [query, undefined];
if (params.where) {
query.setRawCondition(params.where);
}
if (params.authorId) {
query.where('author', '==', User.ptr(params.authorId));
}
if (params.assigneeId) {
addPointersCondition(query, 'assignee', params.assigneeId, User);
}
if (params.groupId) {
addPointersCondition(query, 'group', params.groupId, Group);
}
if (params.reporterId) {
addPointersCondition(query, 'reporter', params.reporterId, User);
}
if (params.participantId) {
query.where('joinedCustomerServices.objectId', 'in', params.participantId);
}
if (categoryIds.size) {
query.where('category.objectId', 'in', Array.from(categoryIds));
}
if (params.status) {
query.where('status', 'in', params.status);
}
if (params['evaluation.star'] !== undefined) {
query.where('evaluation.star', '==', params['evaluation.star']);
}
if (params['evaluation.ts']) {
const [from, to] = params['evaluation.ts'];
if (from) {
query.where('evaluation.ts', '>=', from);
}
if (to) {
query.where('evaluation.ts', '<=', to);
}
})();
}
if (params.createdAtFrom) {
query.where('createdAt', '>=', params.createdAtFrom);
}
if (params.createdAtTo) {
query.where('createdAt', '<=', params.createdAtTo);
}
if (params.tagKey) {
query.where('tags.key', '==', params.tagKey);
}
if (params.tagValue) {
query.where('tags.value', '==', params.tagValue);
}
if (params.privateTagKey) {
if (!(await currentUser.isCustomerService())) {
ctx.throw(403);
}
query.where('privateTags.key', '==', params.privateTagKey);
}
if (params.privateTagValue) {
if (!(await currentUser.isCustomerService())) {
ctx.throw(403);
}
query.where('privateTags.value', '==', params.privateTagValue);
}

if (params.language) {
addInOrNotExistCondition(query, params.language, 'language');
}

query.skip((params.page - 1) * params.pageSize).limit(params.pageSize);
sortItems?.forEach(({ key, order }) => query.orderBy(key, order));

if (params.includeAuthor) {
finalQuery.preload('author');
query.preload('author');
}
if (params.includeReporter) {
finalQuery.preload('reporter');
query.preload('reporter');
}
if (params.includeAssignee) {
finalQuery.preload('assignee');
query.preload('assignee');
}
if (params.includeGroup) {
if (!(await currentUser.isStaff())) {
ctx.throw(403);
}
finalQuery.preload('group');
query.preload('group');
}
if (params.includeFiles) {
finalQuery.preload('files');
query.preload('files');
}
if (params.includeUnreadCount) {
finalQuery.preload('notification', {
query.preload('notification', {
onQuery: (query) => {
return query.where('user', '==', currentUser.toPointer());
},
});
}
if (params.includeFields) {
finalQuery.preload('fieldValue', {
query.preload('fieldValue', {
authOptions: { useMasterKey: true },
});
}

let tickets: Ticket[];
if (params.count && !count) {
const result = await finalQuery.findAndCount(currentUser.getAuthOptions());
if (params.count) {
const result = await query.findAndCount(currentUser.getAuthOptions());
tickets = result[0];
ctx.set('X-Total-Count', result[1].toString());
} else {
tickets = await finalQuery.find(currentUser.getAuthOptions());

if (params.count && count) {
ctx.set('X-Total-Count', count.toString());
}
tickets = await query.find(currentUser.getAuthOptions());
}

if (params.includeCategoryPath) {
Expand Down
16 changes: 8 additions & 8 deletions next/web/src/App/Admin/Tickets/Filter/FilterForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,17 @@ const NormalFieldForm = ({ filters, merge, onSubmit }: FilterFormItemProps<Norma
};

const CustomFieldForm = ({ filters, merge, onSubmit }: FilterFormItemProps<FieldFilters>) => {
const { fieldId: paramFieldId, optionValue, createdAt, textValue } = filters;
const { fieldId: paramFieldId, fieldValue, createdAt } = filters;

const [field, setField] = useState<TicketFieldSchema | undefined>();

const [fieldId, isOptionType, isTextType] = useMemo(
() => [
field?.id ?? paramFieldId,
field ? OptionTypes.includes(field.type) : !!optionValue,
field ? TextTypes.includes(field.type) : !!textValue,
field ? OptionTypes.includes(field.type) : false,
field ? TextTypes.includes(field.type) : false,
],
[field, optionValue, paramFieldId, textValue]
[field, paramFieldId]
);

return (
Expand All @@ -180,16 +180,16 @@ const CustomFieldForm = ({ filters, merge, onSubmit }: FilterFormItemProps<Field
{isOptionType ? (
<OptionFieldValueSelect
fieldId={fieldId}
value={field?.id !== paramFieldId ? undefined : optionValue}
value={field?.id !== paramFieldId ? undefined : fieldValue}
onChange={(v) => {
merge({ fieldId: fieldId, optionValue: v });
merge({ fieldId: fieldId, fieldValue: v });
}}
/>
) : isTextType ? (
<Input
value={field?.id !== paramFieldId ? undefined : textValue}
value={field?.id !== paramFieldId ? undefined : fieldValue}
onChange={(e) => {
merge({ fieldId: fieldId, textValue: e.target.value });
merge({ fieldId: fieldId, fieldValue: e.target.value });
}}
onKeyDown={(e) => e.key === 'Enter' && onSubmit?.()}
/>
Expand Down
8 changes: 3 additions & 5 deletions next/web/src/App/Admin/Tickets/Filter/useTicketFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ export interface NormalFilters extends CommonFilters {
export interface FieldFilters extends CommonFilters {
type: 'field';
fieldId?: string;
optionValue?: string;
textValue?: string;
fieldValue?: string;
}

export type Filters = NormalFilters | FieldFilters;
Expand All @@ -39,7 +38,7 @@ const serializeFilters = (filter: Filters): Record<string, string | undefined> =
if (filter.type === 'field') {
return {
filterType: 'field',
..._.pick(filter, ['fieldId', 'optionValue', 'createdAt', 'textValue']),
..._.pick(filter, ['fieldId', 'fieldValue', 'createdAt']),
};
} else {
return {
Expand Down Expand Up @@ -83,8 +82,7 @@ const deserializeFilters = (params: Record<string, string | undefined>): Filters

// field
'fieldId',
'optionValue',
'textValue',
'fieldValue',
]),
assigneeId: params.assigneeId?.split(','),
groupId: params.groupId?.split(','),
Expand Down
21 changes: 6 additions & 15 deletions next/web/src/App/Admin/Tickets/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,14 @@ function useSmartSearchTickets({
...options
}: UseSmartFetchTicketsOptions) {
const isProcessableSearch = type === 'processable';
const isRegularTicket =
((filters.type === 'normal' && !filters.keyword) ||
(filters.type === 'field' && !!filters.optionValue)) &&
!isProcessableSearch;
const isFieldSearch = filters.type === 'field' && !filters.optionValue && !isProcessableSearch;
const isRegularTicket = filters.type === 'normal' && !filters.keyword && !isProcessableSearch;
const isFieldSearch = filters.type === 'field' && !isProcessableSearch;
const isKeywordSearch = filters.type === 'normal' && !!filters.keyword && !isProcessableSearch;

const dateRange = filters.createdAt && decodeDateRange(filters.createdAt);

const useTicketResult = useTickets({
filters:
filters.type === 'normal'
? _.omit(filters, ['type', 'fieldId', 'optionValue'])
: {
fieldName: (filters as FieldFilters).fieldId,
fieldValue: (filters as FieldFilters).optionValue,
},
filters: _.omit(filters, ['type', 'fieldId', 'fieldValue']),
...options,
queryOptions: {
...queryOptions,
Expand All @@ -71,7 +62,7 @@ function useSmartSearchTickets({
});

const useSearchTicketsResult = useSearchTickets((filters as NormalFilters).keyword!, {
filters: _.omit(filters, ['keyword', 'type', 'fieldId', 'optionValue']) as NormalFilters,
filters: _.omit(filters, ['keyword', 'type', 'fieldId', 'fieldValue']) as NormalFilters,
...options,
queryOptions: {
...queryOptions,
Expand All @@ -87,12 +78,12 @@ function useSmartSearchTickets({
},
});

const { fieldId, textValue } = filters as FieldFilters;
const { fieldId, fieldValue } = filters as FieldFilters;

const useFieldSearchResult = useSearchTicketCustomField(
{
fieldId,
fieldValue: textValue,
fieldValue,
createdAt: dateRange ? [dateRange.from, dateRange.to] : undefined,
},
{
Expand Down

0 comments on commit 341ab01

Please sign in to comment.