Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { ScreeningThreshold } from '@app-builder/components/ScreeningThreshold';
import { Spinner } from '@app-builder/components/Spinner';
import { SEARCH_ENTITIES } from '@app-builder/constants/screening-entity';
import { type ScreeningMatchPayload, ScreeningProviders } from '@app-builder/models/screening';
import { useFreeformSearchMutation } from '@app-builder/queries/screening/freeform-search';
import {
useCreateFreeFormSearchPresetMutation,
useFreeformSearchMutation,
useListFreeFormSearchPresetsQuery,
} from '@app-builder/queries/screening/freeform-search';
import { type ListConfigFilters, useListConfigQuery } from '@app-builder/queries/screening/lists-config';
import { type FreeformSearchInput } from '@app-builder/server-fns/screenings';
import { useOrganizationDetails } from '@app-builder/services/organization/organization-detail';
Expand All @@ -18,7 +22,7 @@ import { createContext, type FunctionComponent, useContext, useEffect, useMemo,
import toast from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { match } from 'ts-pattern';
import { Button, Input } from 'ui-design-system';
import { Button, Input, Popover, SelectV2 } from 'ui-design-system';
import { Icon } from 'ui-icons';
import { screeningsI18n } from '../screenings-i18n';
import { setAdditionalFields } from '../set-additional-fields';
Expand Down Expand Up @@ -91,6 +95,13 @@ const FreeformSearchFormInner: FunctionComponent<{ provider: ScreeningProviders
return getCanonicalSelectedKeys(initial);
});
const selectedDatasetsKey = useMemo(() => selectedDatasets.toSorted().join(','), [selectedDatasets]);
const listFreeFormSearchPresetsQuery = useListFreeFormSearchPresetsQuery();
const [selectedPreset, setSelectedPreset] = useState<string | undefined>(undefined);
const [savePresetPopoverOpen, setSavePresetPopoverOpen] = useState(false);
const [presetName, setPresetName] = useState('');
const [presetNameError, setPresetNameError] = useState<string | undefined>(undefined);

const createFreeFormSearchPresetMutation = useCreateFreeFormSearchPresetMutation();

const listSharp = ListAndTopicDatasetConfiguration.createSharp({
datasets: makeDatasetsMap(selectedDatasets),
Expand Down Expand Up @@ -140,9 +151,42 @@ const FreeformSearchFormInner: FunctionComponent<{ provider: ScreeningProviders
const handleClearFilters = () => {
form.reset();
setSelectedDatasets([]);
// setSelectedPreset(undefined);
originalLimit.current = DEFAULT_LIMIT;
};

const handleSaveFilters = async () => {
const trimmedName = presetName.trim();
if (!trimmedName) {
setPresetNameError(t('screenings:freeform_search.preset_name_required'));
return;
}
const result = await createFreeFormSearchPresetMutation.mutateAsync({
name: trimmedName,
value: {
datasets: selectedDatasets,
threshold,
limit,
},
});
if (result.success) {
listFreeFormSearchPresetsQuery.refetch();
setSavePresetPopoverOpen(false);
setPresetName('');
setPresetNameError(undefined);
} else {
toast.error(t('common:errors.unknown'));
}
};

const handleSavePresetPopoverChange = (isOpen: boolean) => {
setSavePresetPopoverOpen(isOpen);
if (!isOpen) {
setPresetName('');
setPresetNameError(undefined);
}
};

const hasActiveFilters =
selectedDatasets.length > 0 ||
(entityType && entityType !== 'Thing') ||
Expand Down Expand Up @@ -187,6 +231,15 @@ const FreeformSearchFormInner: FunctionComponent<{ provider: ScreeningProviders
</div>
<ListAndTopicDatasetConfiguration.Provider value={listSharp}>
<div className="bg-surface-card border-grey-border rounded-lg border p-md space-y-md">
{listFreeFormSearchPresetsQuery?.data?.length ? (
<SelectV2
options={listFreeFormSearchPresetsQuery.data.map((preset) => ({ label: preset, value: preset }))}
placeholder={t('screenings:freeform_search.preset_placeholder')}
value={selectedPreset}
onChange={(value) => setSelectedPreset(value)}
className="w-full"
/>
) : null}
<ScreeningThreshold
threshold={threshold}
onChange={(value) => {
Expand Down Expand Up @@ -216,6 +269,36 @@ const FreeformSearchFormInner: FunctionComponent<{ provider: ScreeningProviders
<Button variant="secondary" appearance="stroked" size="medium" onClick={handleClearFilters}>
{t('screenings:freeform_search.clear_filters')}
</Button>
<Popover.Root open={savePresetPopoverOpen} onOpenChange={handleSavePresetPopoverChange}>
<Popover.Trigger asChild>
<Button variant="primary" appearance="stroked" size="medium">
{t('screenings:freeform_search.save_filters')}
</Button>
</Popover.Trigger>
<Popover.Content side="bottom" align="end" sideOffset={4} className="w-[280px] p-4">
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
handleSaveFilters();
}}
className="flex flex-col gap-2"
>
<Input
autoFocus
value={presetName}
onChange={(e) => {
setPresetName(e.target.value);
if (presetNameError) setPresetNameError(undefined);
}}
placeholder={t('screenings:freeform_search.preset_name_placeholder')}
borderColor={presetNameError ? 'redfigma-47' : 'greyfigma-90'}
disabled={createFreeFormSearchPresetMutation.isPending}
/>
{presetNameError ? <span className="text-red-primary text-xs">{presetNameError}</span> : null}
</form>
</Popover.Content>
</Popover.Root>
</div>
)}
<form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]}>
Expand Down
4 changes: 4 additions & 0 deletions packages/app-builder/src/locales/ar/screenings.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,13 @@
"freeform_search.new_search": "بحث جديد",
"freeform_search.no_results_description": "حاول تعديل معايير البحث أو خفض حد التشابه.",
"freeform_search.no_results_title": "لم يتم العثور على مطابقات",
"freeform_search.preset_name_placeholder": "اسم الإعداد المسبق",
"freeform_search.preset_name_required": "الاسم مطلوب",
"freeform_search.preset_placeholder": "اختر مجموعة محددة...",
"freeform_search.results_count_one": "تم العثور على {{count}} مطابقة",
"freeform_search.results_count_other": "تم العثور على {{count}} مطابقات",
"freeform_search.results_title": "نتائج البحث",
"freeform_search.save_filters": "حفظ",
"freeform_search.save.button": "حفظ النتائج",
"freeform_search.save.datasets": "مجموعات البيانات",
"freeform_search.save.description": "يمكنك حفظ المرشحات لتعمل بشكل أسرع في المرة القادمة.",
Expand Down
4 changes: 4 additions & 0 deletions packages/app-builder/src/locales/en/screenings.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,13 @@
"freeform_search.new_search": "New Search",
"freeform_search.no_results_description": "Try adjusting your search criteria or lowering the similarity threshold.",
"freeform_search.no_results_title": "No matches found",
"freeform_search.preset_name_placeholder": "Preset name",
"freeform_search.preset_name_required": "Name is required",
"freeform_search.preset_placeholder": "Select a preset...",
"freeform_search.results_count_one": "{{count}} match found",
"freeform_search.results_count_other": "{{count}} matches found",
"freeform_search.results_title": "Search Results",
"freeform_search.save_filters": "Save",
"freeform_search.save.button": "Save results",
"freeform_search.save.datasets": "Datasets",
"freeform_search.save.description": "You can save the filters to go faster next time.",
Expand Down
4 changes: 4 additions & 0 deletions packages/app-builder/src/locales/fr/screenings.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,13 @@
"freeform_search.new_search": "Nouvelle recherche",
"freeform_search.no_results_description": "Essayez d'ajuster vos critères de recherche ou de baisser le seuil de similarité.",
"freeform_search.no_results_title": "Aucune correspondance trouvée",
"freeform_search.preset_name_placeholder": "Nom du préréglage",
"freeform_search.preset_name_required": "Le nom est requis",
"freeform_search.preset_placeholder": "Sélectionner un préréglage...",
"freeform_search.results_count_one": "{{count}} correspondance trouvée",
"freeform_search.results_count_other": "{{count}} correspondances trouvées",
"freeform_search.results_title": "Résultats de recherche",
"freeform_search.save_filters": "Enregistrer",
"freeform_search.save.button": "Sauvegarder les résultats",
"freeform_search.save.datasets": "Listes",
"freeform_search.save.description": "Vous pouvez enregistrer les filtres pour aller plus vite la prochaine fois.",
Expand Down
52 changes: 52 additions & 0 deletions packages/app-builder/src/queries/screening/freeform-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import {
type ScreeningMatchPayload,
} from '@app-builder/models/screening';
import {
createFreeFormSearchPresetFn,
deleteFreeFormSearchPresetFn,
type FreeformSearchInput,
FreeformSearchPreset,
freeformSearchFn,
getFreeFormSearchPresetFn,
getFreeformSearchFn,
getListFreeFormSearchPresetsFn,
listSavedFreeformSearchesFn,
saveFreeformSearchFn,
} from '@app-builder/server-fns/screenings';
Expand Down Expand Up @@ -75,3 +80,50 @@ export const useGetFreeformSearchQuery = (id: string) => {
},
});
};

export const useListFreeFormSearchPresetsQuery = () => {
const getListFFS = useServerFn(getListFreeFormSearchPresetsFn);

return useQuery({
queryKey: ['screening', 'freeform-search'],
queryFn: async () => {
const result = await getListFFS();
return result;
},
});
};

export const useGetFreeFormSearchPresetQuery = (name: string) => {
const getFreeFormSearchPreset = useServerFn(getFreeFormSearchPresetFn);

return useQuery({
queryKey: ['screening', 'freeform-search', 'preset', name],
queryFn: async () => {
const result = await getFreeFormSearchPreset({ data: { name } });
return result;
},
});
};

export const useCreateFreeFormSearchPresetMutation = () => {
const createFreeFormSearchPreset = useServerFn(createFreeFormSearchPresetFn);

return useMutation({
mutationKey: ['screening', 'freeform-search', 'preset'],
mutationFn: async ({ name, value }: { name: string; value: FreeformSearchPreset }) => {
console.log('createFreeFormSearchPreset', { name, value });
return createFreeFormSearchPreset({ data: { name, value } });
},
});
};

export const useDeleteFreeFormSearchPresetMutation = () => {
const deleteFreeFormSearchPreset = useServerFn(deleteFreeFormSearchPresetFn);

return useMutation({
mutationKey: ['screening', 'freeform-search', 'preset'],
mutationFn: async ({ name }: { name: string }) => {
return deleteFreeFormSearchPreset({ data: { name } });
},
});
};
47 changes: 47 additions & 0 deletions packages/app-builder/src/server-fns/screenings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,50 @@ export const uploadScreeningFileFn = createServerFn({ method: 'POST' })
headers,
});
});

/** Freeform Search Presets */

const FreeformSearchPresetSchema = z.object({
datasets: z.array(z.string()).optional(),
threshold: z.number().min(0).max(100).optional(),
limit: z.number().min(10).max(50).optional(),
});

export type FreeformSearchPreset = z.infer<typeof FreeformSearchPresetSchema>;
// TODO: replace with the endpoint call when available
const FFSMap = new Map<string, FreeformSearchPreset>();

export const getListFreeFormSearchPresetsFn = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.handler(async ({ context }) => {
// const result = await context.authInfo.screening.getListFreeFormSearch();
const result = Array.from(FFSMap.keys());
return result;
});

export const getFreeFormSearchPresetFn = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.inputValidator(z.object({ name: z.string() }))
.handler(async ({ context, data }) => {
// const result = await context.authInfo.screening.getFreeFormSearchPreset();
const result = FFSMap.get(data.name);
return result;
});

export const createFreeFormSearchPresetFn = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
.inputValidator(z.object({ name: z.string(), value: FreeformSearchPresetSchema }))
.handler(async ({ context, data }) => {
// const result = await context.authInfo.screening.createFreeFormSearchPreset();
FFSMap.set(data.name, data.value);
return { success: true };
});

export const deleteFreeFormSearchPresetFn = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
.inputValidator(z.object({ name: z.string() }))
.handler(async ({ context, data }) => {
// const result = await context.authInfo.screening.deleteFreeFormSearchPreset();
FFSMap.delete(data.name);
return { success: true };
});
3 changes: 0 additions & 3 deletions packages/ui-design-system/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
docs: {
autodocs: 'tag',
},
core: {
disableTelemetry: true,
},
Expand Down
1 change: 0 additions & 1 deletion packages/ui-design-system/src/Radio/Radio.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const OPTIONS = ['option1', 'option2', 'option3'] as const;

const Story: Meta<Args> = {
title: 'Radio',
component: Radio.Root,
args: {
size: 'regular',
disabledItems: [],
Expand Down
1 change: 0 additions & 1 deletion packages/ui-design-system/src/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { SelectV2, type SelectV2Props } from './Select';
type StoryProps = Pick<SelectV2Props<string>, 'disabled' | 'placeholder'>;

const Story: Meta<StoryProps> = {
component: SelectV2,
title: 'SelectV2',
args: {
placeholder: 'Select a value...',
Expand Down
Loading