refactor: reuse FkMultiSelectField for report period/payee selectors

Export FkMultiSelectField from react-openapi and use it directly in the
report viewer for the Period/Payee dimension filters instead of the
custom OptionMultiSelect component, gaining the admin panel's QoL
behavior (no close-on-click, selected-first with tick marks, chips).
Feed it fabricated FieldConfigs plus fkOptions derived from the report's
metadata.group_options; delete the standalone OptionMultiSelect.
This commit is contained in:
2026-08-20 16:20:06 +05:30
parent f9759e3968
commit fc3a48a367
3 changed files with 56 additions and 69 deletions

View File

@@ -6,6 +6,7 @@ export { useResource } from "./src/context/useResource";
export { ListCellRenderer, DetailFieldRenderer, applyDisplayFormat } from "./src/components/fields";
export { FormFieldRenderer } from "./src/components/fields/FormFieldRenderer";
export { CurrencyField, formatCurrency } from "./src/components/fields/renderers/CurrencyField";
export { FkMultiSelectField } from "./src/components/fields/renderers/FkMultiSelectField";
export { SseStreamView } from "./src/components/SseStreamView";
export { SseConnectionStatus } from "./src/components/SseConnectionStatus";
export { getApi } from "./src/hooks/useApi";

View File

@@ -10,14 +10,42 @@ import {
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import CachedIcon from "@mui/icons-material/Cached";
import { useResource, formatCurrency } from "../../react-openapi";
import { FkMultiSelectField, useResource, formatCurrency } from "../../react-openapi";
import type { FieldConfig } from "../../react-openapi";
import { StatCard } from "../common/components/StatCard";
import { TransactionList } from "../common/components/TransactionList";
import { OptionMultiSelect } from "../common/components/OptionMultiSelect";
import type { TxnFieldConfigs } from "../common/types";
import { toPeriodGranularity } from "../common/utils/transactions";
import { aggregateSlice } from "./types";
const periodField: FieldConfig = {
name: "period",
label: "Period",
description: "",
type: "string",
order: 0,
hidden: {},
filterable: true,
sortable: false,
readOnly: false,
required: false,
isArray: true,
};
const payeeField: FieldConfig = {
name: "payee",
label: "Payee",
description: "",
type: "string",
order: 0,
hidden: {},
filterable: true,
sortable: false,
readOnly: false,
required: false,
isArray: true,
};
interface ReportViewerProps {
id: string;
version: number;
@@ -70,6 +98,15 @@ export function ReportViewer({ id, version, fields, onClose, onRegenerated }: Re
};
}, [report]);
const periodOptions = useMemo(
() => groupOptions.period.map((label: string) => ({ value: label, label })),
[groupOptions],
);
const payeeOptions = useMemo(
() => groupOptions.payee.map((label: string) => ({ value: label, label })),
[groupOptions],
);
const slice = useMemo(
() => aggregateSlice(data ?? [], { periods: selectedPeriods, payees: selectedPayees }),
[data, selectedPeriods, selectedPayees],
@@ -149,18 +186,22 @@ export function ReportViewer({ id, version, fields, onClose, onRegenerated }: Re
) : (
<Box sx={{ p: 2.5 }}>
<Box sx={{ display: "flex", gap: 1.5, flexWrap: "wrap", mb: 2.5 }}>
<OptionMultiSelect
label="Period"
options={groupOptions.period}
value={selectedPeriods}
onChange={setSelectedPeriods}
/>
<OptionMultiSelect
label="Payee"
options={groupOptions.payee}
value={selectedPayees}
onChange={setSelectedPayees}
/>
<Box sx={{ width: 260 }}>
<FkMultiSelectField
field={{ ...periodField, readOnly: groupOptions.period.length === 0 }}
fkOptions={periodOptions}
value={selectedPeriods}
onChange={setSelectedPeriods}
/>
</Box>
<Box sx={{ width: 260 }}>
<FkMultiSelectField
field={{ ...payeeField, readOnly: groupOptions.payee.length === 0 }}
fkOptions={payeeOptions}
value={selectedPayees}
onChange={setSelectedPayees}
/>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ alignSelf: "center" }}>
Showing {slice.count} transactions across {slice.txns.length} rows
</Typography>

View File

@@ -1,55 +0,0 @@
import React, { useMemo } from "react";
import { Autocomplete, TextField, Chip, Box } from "@mui/material";
import DoneIcon from "@mui/icons-material/Done";
interface OptionMultiSelectProps {
label: string;
options: string[];
value: string[];
onChange: (value: string[]) => void;
}
export function OptionMultiSelect({ label, options, value, onChange }: OptionMultiSelectProps) {
const sortedOptions = useMemo(() => {
const sel = new Set(value);
const picked: string[] = [];
const rest: string[] = [];
for (const opt of options) {
(sel.has(opt) ? picked : rest).push(opt);
}
return [...picked, ...rest];
}, [options, value]);
return (
<Autocomplete
multiple
size="small"
sx={{ width: 260 }}
options={sortedOptions}
value={value}
onChange={(_, newVal) => onChange(newVal)}
disabled={options.length === 0}
renderOption={(props, option, { selected }) => (
<li {...props}>
{selected ? <DoneIcon sx={{ fontSize: 14, mr: 1, color: "primary.main" }} /> : <Box sx={{ width: 22, mr: 1 }} />}
{option}
</li>
)}
renderTags={(tagValue, getTagProps) => {
const maxChips = 1;
return (
<>
{tagValue.slice(0, maxChips).map((tag, index) => {
const { key, ...tagProps } = getTagProps({ index });
return <Chip key={key} {...tagProps} label={tag.length > 12 ? `${tag.slice(0, 10)}..` : tag} size="small" />;
})}
{tagValue.length > maxChips && <Chip label={`+${tagValue.length - maxChips}`} size="small" />}
</>
);
}}
renderInput={(params) => (
<TextField {...params} label={label} placeholder={value.length === 0 ? "All" : undefined} />
)}
/>
);
}