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..49849f3e2 100644 --- a/src/apps/form-store-service.ts +++ b/src/apps/form-store-service.ts @@ -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/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/ColumnFilters.tsx b/src/components/formStore/table/ColumnFilters.tsx index 7d2104e7b..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': { @@ -315,6 +319,24 @@ function ColumnFilters({ filter }: Props) { /> ) } + case 'FORMS_APP_ID': { + return ( + { + filter.onChange( + newValue.length + ? { + $in: newValue, + } + : undefined, + false, + ) + }} + /> + ) + } default: { return null } @@ -323,6 +345,72 @@ function ColumnFilters({ filter }: Props) { export default React.memo(ColumnFilters) +const NO_APP_SENTINEL = '__no_app__' + +function FormsAppIdTextField({ + options, + value, + onChange, +}: { + options: Array<{ value: 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.value === null + ? NO_APP_SENTINEL + : option.value.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.value === null ? NO_APP_SENTINEL : option.value.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..0530a32ce 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(() => { @@ -57,6 +59,18 @@ export default function useFormStoreTable({ }, [form, onChangeParameters]) const formElements = React.useContext(FormStoreElementsContext) + + const formsAppOptions = React.useMemo(() => { + if (!getFormsAppLabel) return undefined + return [ + ...form.formsAppIds.map((id) => ({ + value: id, + label: getFormsAppLabel(id), + })), + { value: null, label: getFormsAppLabel(null) }, + ] + }, [form.formsAppIds, getFormsAppLabel]) + const columns = React.useMemo(() => { return generateColumns({ sorting: parameters.sorting, @@ -258,6 +272,56 @@ export default function useFormStoreTable({ ), }, + ...(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, + ) + }, + }, + }, + cell: ({ + row: { original: formStoreRecord }, + }: { + row: { original: SubmissionTypes.FormStoreRecord } + }) => { + const text = getFormsAppLabel( + formStoreRecord.formsAppId ?? null, + ) + return ( + <> + {text} + + + ) + }, + }, + ] + : []), { id: 'TASK_GROUP', header: 'Task Group', @@ -432,6 +496,8 @@ export default function useFormStoreTable({ }) }, [ formElements, + formsAppOptions, + getFormsAppLabel, onChangeParameters, parameters, submissionIdValidationMessage, diff --git a/src/types/react-table.d.ts b/src/types/react-table.d.ts index b4faf5f31..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 { @@ -69,6 +69,13 @@ declare module '@tanstack/react-table' { } } } + | { + type: 'FORMS_APP_ID' + options: Array + value?: { + $in: (number | null)[] + } + } ) }