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:
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user