From 28b1987083c86d6a69128787cabe3f1f33471424 Mon Sep 17 00:00:00 2001 From: Zac Turner Date: Wed, 6 May 2026 15:52:11 +1000 Subject: [PATCH 1/3] ap-8349 # add formsAppId column and filter to Form Store table Allows users to filter submissions by the app used to submit. Supports multi-select with nullable values for appless submissions. Co-authored-by: Cursor --- CHANGELOG.md | 1 + src/apps/form-store-service.ts | 6 +- .../formStore/table/ColumnFilters.tsx | 84 +++++++++++++++++++ .../formStore/table/useFormStoreTable.tsx | 49 +++++++++++ src/types/react-table.d.ts | 7 ++ 5 files changed, 146 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6e00435f..fc381debc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - support adhoc tasks +- `formsAppId` column and filter to Form Store table ## [10.1.1] - 2026-04-23 diff --git a/src/apps/form-store-service.ts b/src/apps/form-store-service.ts index 313ee553b..725f2a3ba 100644 --- a/src/apps/form-store-service.ts +++ b/src/apps/form-store-service.ts @@ -19,7 +19,7 @@ export type FormStoreFilter = { $gt?: T $lt?: T $lte?: T - $in?: string[] + $in?: Array $elemMatch?: { $in?: string[] } @@ -59,6 +59,10 @@ export type FormStoreFilters = { /** Filter results by the task group instance label */ label?: FormStoreFilter } + /** Filter results by the forms app used to submit */ + formsAppId?: { + $in: (number | null)[] + } } export type FormStoreParameters = { diff --git a/src/components/formStore/table/ColumnFilters.tsx b/src/components/formStore/table/ColumnFilters.tsx index 7d2104e7b..7a385e41e 100644 --- a/src/components/formStore/table/ColumnFilters.tsx +++ b/src/components/formStore/table/ColumnFilters.tsx @@ -315,6 +315,24 @@ function ColumnFilters({ filter }: Props) { /> ) } + case 'FORMS_APP_ID': { + return ( + { + filter.onChange( + newValue.length + ? { + $in: newValue, + } + : undefined, + false, + ) + }} + /> + ) + } default: { return null } @@ -323,6 +341,72 @@ function ColumnFilters({ filter }: Props) { export default React.memo(ColumnFilters) +const NO_APP_SENTINEL = '__no_app__' + +function FormsAppIdTextField({ + options, + value, + onChange, +}: { + options: Array<{ formsAppId: number | null; label: string }> + value: (number | null)[] | undefined + onChange: (newValue: (number | null)[]) => void +}) { + const selectedStrings = React.useMemo( + () => + value?.map((v) => (v === null ? NO_APP_SENTINEL : v.toString())) ?? [], + [value], + ) + + return ( + { + return options + .reduce((selectedLabels, option) => { + const key = + option.formsAppId === null + ? NO_APP_SENTINEL + : option.formsAppId.toString() + if ((selectedIds as string[]).includes(key)) { + selectedLabels.push(option.label) + } + return selectedLabels + }, []) + .join(', ') + }, + }} + fullWidth + value={selectedStrings} + onChange={(e: React.ChangeEvent) => { + const selected = e.target.value as unknown as string[] + onChange( + selected.map((v) => (v === NO_APP_SENTINEL ? null : parseInt(v, 10))), + ) + }} + > + {options.map((option) => { + const key = + option.formsAppId === null + ? NO_APP_SENTINEL + : option.formsAppId.toString() + return ( + + + {option.label} + + ) + })} + + ) +} + function OptionsTextField({ options, value, diff --git a/src/components/formStore/table/useFormStoreTable.tsx b/src/components/formStore/table/useFormStoreTable.tsx index 73d752312..f627888cb 100644 --- a/src/components/formStore/table/useFormStoreTable.tsx +++ b/src/components/formStore/table/useFormStoreTable.tsx @@ -57,6 +57,17 @@ export default function useFormStoreTable({ }, [form, onChangeParameters]) const formElements = React.useContext(FormStoreElementsContext) + + const formsAppOptions = React.useMemo(() => { + const options: Array<{ formsAppId: number | null; label: string }> = + form.formsAppIds.map((id) => ({ + formsAppId: id, + label: id.toString(), + })) + options.push({ formsAppId: null, label: 'No App' }) + return options + }, [form.formsAppIds]) + const columns = React.useMemo(() => { return generateColumns({ sorting: parameters.sorting, @@ -258,6 +269,43 @@ export default function useFormStoreTable({ ), }, + { + id: 'FORMS_APP_ID', + header: 'App', + meta: { + sorting: undefined, + filter: { + type: 'FORMS_APP_ID' as const, + options: formsAppOptions, + value: parameters.filters?.formsAppId as + | { $in: (number | null)[] } + | undefined, + onChange: (newValue) => { + onChangeParameters( + (currentParameters) => ({ + ...currentParameters, + filters: { + ...currentParameters.filters, + formsAppId: newValue as + | { $in: (number | null)[] } + | undefined, + }, + }), + false, + ) + }, + }, + }, + cell: ({ row: { original: formStoreRecord } }) => { + const text = formStoreRecord.formsAppId?.toString() ?? 'No App' + return ( + <> + {text} + + + ) + }, + }, { id: 'TASK_GROUP', header: 'Task Group', @@ -432,6 +480,7 @@ export default function useFormStoreTable({ }) }, [ formElements, + formsAppOptions, onChangeParameters, parameters, submissionIdValidationMessage, diff --git a/src/types/react-table.d.ts b/src/types/react-table.d.ts index b4faf5f31..8e745941e 100644 --- a/src/types/react-table.d.ts +++ b/src/types/react-table.d.ts @@ -69,6 +69,13 @@ declare module '@tanstack/react-table' { } } } + | { + type: 'FORMS_APP_ID' + options: Array<{ formsAppId: number | null; label: string }> + value?: { + $in: (number | null)[] + } + } ) } From 9136b3ddb58f7c1e137f676b5de85fbb57205bfa Mon Sep 17 00:00:00 2001 From: Zac Turner Date: Wed, 6 May 2026 16:38:50 +1000 Subject: [PATCH 2/3] ap-8349 # make formsAppId column opt-in via getFormsAppLabel prop The column and filter only appear when getFormsAppLabel is provided to OneBlinkFormStoreProvider. The consumer controls label resolution. Co-authored-by: Cursor --- src/apps/form-store-service.ts | 2 +- .../formStore/FormStoreTableProvider.tsx | 3 + .../formStore/OneBlinkFormStoreProvider.tsx | 6 +- .../formStore/table/useFormStoreTable.tsx | 103 ++++++++++-------- 4 files changed, 69 insertions(+), 45 deletions(-) diff --git a/src/apps/form-store-service.ts b/src/apps/form-store-service.ts index 725f2a3ba..49849f3e2 100644 --- a/src/apps/form-store-service.ts +++ b/src/apps/form-store-service.ts @@ -19,7 +19,7 @@ export type FormStoreFilter = { $gt?: T $lt?: T $lte?: T - $in?: Array + $in?: string[] $elemMatch?: { $in?: string[] } diff --git a/src/components/formStore/FormStoreTableProvider.tsx b/src/components/formStore/FormStoreTableProvider.tsx index c5b1a69c6..534db0d3b 100644 --- a/src/components/formStore/FormStoreTableProvider.tsx +++ b/src/components/formStore/FormStoreTableProvider.tsx @@ -29,9 +29,11 @@ function getParamsFromLocalStorage() { export function FormStoreTableProvider({ form, children, + getFormsAppLabel, }: { form: FormTypes.Form children: React.ReactNode + getFormsAppLabel?: (formsAppId: number | null) => string }) { const history = useHistory() const location = useLocation() @@ -142,6 +144,7 @@ export function FormStoreTableProvider({ submissionIdValidationMessage, form, onRefresh, + getFormsAppLabel, }) const visibleColumns = formStoreTable.getVisibleFlatColumns() diff --git a/src/components/formStore/OneBlinkFormStoreProvider.tsx b/src/components/formStore/OneBlinkFormStoreProvider.tsx index e3095ebae..070750603 100644 --- a/src/components/formStore/OneBlinkFormStoreProvider.tsx +++ b/src/components/formStore/OneBlinkFormStoreProvider.tsx @@ -19,9 +19,11 @@ export const FormStoreElementsContext = export function OneBlinkFormStoreProvider({ form, children, + getFormsAppLabel, }: { form: FormTypes.Form children: React.ReactNode + getFormsAppLabel?: (formsAppId: number | null) => string }) { const fetchFormStoreDefinition = React.useCallback( (abortSignal?: AbortSignal) => { @@ -55,7 +57,9 @@ export function OneBlinkFormStoreProvider({ - {children} + + {children} + ) } diff --git a/src/components/formStore/table/useFormStoreTable.tsx b/src/components/formStore/table/useFormStoreTable.tsx index f627888cb..5991ac877 100644 --- a/src/components/formStore/table/useFormStoreTable.tsx +++ b/src/components/formStore/table/useFormStoreTable.tsx @@ -31,6 +31,7 @@ export default function useFormStoreTable({ onChangeParameters, onRefresh, submissionIdValidationMessage, + getFormsAppLabel, }: { formStoreRecords: SubmissionTypes.FormStoreRecord[] form: FormTypes.Form @@ -38,6 +39,7 @@ export default function useFormStoreTable({ onChangeParameters: OnChangeFilters onRefresh: () => void submissionIdValidationMessage?: string + getFormsAppLabel?: (formsAppId: number | null) => string }) { // Resets parameters on form change React.useEffect(() => { @@ -59,14 +61,15 @@ export default function useFormStoreTable({ const formElements = React.useContext(FormStoreElementsContext) const formsAppOptions = React.useMemo(() => { - const options: Array<{ formsAppId: number | null; label: string }> = - form.formsAppIds.map((id) => ({ - formsAppId: id, - label: id.toString(), - })) - options.push({ formsAppId: null, label: 'No App' }) - return options - }, [form.formsAppIds]) + if (!getFormsAppLabel) return undefined + return [ + ...form.formsAppIds.map((id) => ({ + formsAppId: id as number | null, + label: getFormsAppLabel(id), + })), + { formsAppId: null as number | null, label: getFormsAppLabel(null) }, + ] + }, [form.formsAppIds, getFormsAppLabel]) const columns = React.useMemo(() => { return generateColumns({ @@ -269,43 +272,56 @@ export default function useFormStoreTable({ ), }, - { - id: 'FORMS_APP_ID', - header: 'App', - meta: { - sorting: undefined, - filter: { - type: 'FORMS_APP_ID' as const, - options: formsAppOptions, - value: parameters.filters?.formsAppId as - | { $in: (number | null)[] } - | undefined, - onChange: (newValue) => { - onChangeParameters( - (currentParameters) => ({ - ...currentParameters, - filters: { - ...currentParameters.filters, - formsAppId: newValue as - | { $in: (number | null)[] } - | undefined, + ...(formsAppOptions && getFormsAppLabel + ? [ + { + id: 'FORMS_APP_ID', + header: 'App', + meta: { + sorting: undefined, + filter: { + type: 'FORMS_APP_ID' as const, + options: formsAppOptions, + value: parameters.filters?.formsAppId as + | { $in: (number | null)[] } + | undefined, + onChange: ( + newValue?: formStoreService.FormStoreFilter, + shouldDebounce?: boolean, + ) => { + onChangeParameters( + (currentParameters) => ({ + ...currentParameters, + filters: { + ...currentParameters.filters, + formsAppId: newValue as + | { $in: (number | null)[] } + | undefined, + }, + }), + shouldDebounce ?? false, + ) }, - }), - false, - ) + }, + }, + cell: ({ + row: { original: formStoreRecord }, + }: { + row: { original: SubmissionTypes.FormStoreRecord } + }) => { + const text = getFormsAppLabel( + formStoreRecord.formsAppId ?? null, + ) + return ( + <> + {text} + + + ) + }, }, - }, - }, - cell: ({ row: { original: formStoreRecord } }) => { - const text = formStoreRecord.formsAppId?.toString() ?? 'No App' - return ( - <> - {text} - - - ) - }, - }, + ] + : []), { id: 'TASK_GROUP', header: 'Task Group', @@ -481,6 +497,7 @@ export default function useFormStoreTable({ }, [ formElements, formsAppOptions, + getFormsAppLabel, onChangeParameters, parameters, submissionIdValidationMessage, From c0926ae94a43979be574a5555c0836e97e0e9c2f Mon Sep 17 00:00:00 2001 From: Zac Turner Date: Thu, 7 May 2026 09:38:33 +1000 Subject: [PATCH 3/3] ap-8349 # Tweaks --- .../formStore/table/ColumnFilters.tsx | 42 ++++++++++--------- .../formStore/table/useFormStoreTable.tsx | 4 +- src/types/react-table.d.ts | 4 +- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/components/formStore/table/ColumnFilters.tsx b/src/components/formStore/table/ColumnFilters.tsx index 7a385e41e..a56c403fb 100644 --- a/src/components/formStore/table/ColumnFilters.tsx +++ b/src/components/formStore/table/ColumnFilters.tsx @@ -28,6 +28,10 @@ type Props = { const shortDateFormat = localisationService.getDateFnsFormats().shortDate +export type FormsAppOption = { + value: number | null + label: string +} function ColumnFilters({ filter }: Props) { switch (filter.type) { case 'SUBMISSION_ID': { @@ -348,7 +352,7 @@ function FormsAppIdTextField({ value, onChange, }: { - options: Array<{ formsAppId: number | null; label: string }> + options: Array<{ value: number | null; label: string }> value: (number | null)[] | undefined onChange: (newValue: (number | null)[]) => void }) { @@ -365,21 +369,23 @@ function FormsAppIdTextField({ size="small" label="Filter" select - SelectProps={{ - multiple: true, - renderValue: (selectedIds: unknown) => { - return options - .reduce((selectedLabels, option) => { - const key = - option.formsAppId === null - ? NO_APP_SENTINEL - : option.formsAppId.toString() - if ((selectedIds as string[]).includes(key)) { - selectedLabels.push(option.label) - } - return selectedLabels - }, []) - .join(', ') + slotProps={{ + select: { + multiple: true, + renderValue: (selectedIds) => { + return options + .reduce((selectedLabels, option) => { + const key = + option.value === null + ? NO_APP_SENTINEL + : option.value.toString() + if ((selectedIds as string[]).includes(key)) { + selectedLabels.push(option.label) + } + return selectedLabels + }, []) + .join(', ') + }, }, }} fullWidth @@ -393,9 +399,7 @@ function FormsAppIdTextField({ > {options.map((option) => { const key = - option.formsAppId === null - ? NO_APP_SENTINEL - : option.formsAppId.toString() + option.value === null ? NO_APP_SENTINEL : option.value.toString() return ( diff --git a/src/components/formStore/table/useFormStoreTable.tsx b/src/components/formStore/table/useFormStoreTable.tsx index 5991ac877..0530a32ce 100644 --- a/src/components/formStore/table/useFormStoreTable.tsx +++ b/src/components/formStore/table/useFormStoreTable.tsx @@ -64,10 +64,10 @@ export default function useFormStoreTable({ if (!getFormsAppLabel) return undefined return [ ...form.formsAppIds.map((id) => ({ - formsAppId: id as number | null, + value: id, label: getFormsAppLabel(id), })), - { formsAppId: null as number | null, label: getFormsAppLabel(null) }, + { value: null, label: getFormsAppLabel(null) }, ] }, [form.formsAppIds, getFormsAppLabel]) diff --git a/src/types/react-table.d.ts b/src/types/react-table.d.ts index 8e745941e..a6b4e6e5b 100644 --- a/src/types/react-table.d.ts +++ b/src/types/react-table.d.ts @@ -1,6 +1,6 @@ import { FormTypes } from '@oneblink/types' import { formStoreService } from 'apps' - +import { FormsAppOption } from '../components/formStore/table/ColumnFilters' declare module '@tanstack/react-table' { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface ColumnMeta { @@ -71,7 +71,7 @@ declare module '@tanstack/react-table' { } | { type: 'FORMS_APP_ID' - options: Array<{ formsAppId: number | null; label: string }> + options: Array value?: { $in: (number | null)[] }