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 { ListCellRenderer, DetailFieldRenderer, applyDisplayFormat } from "./src/components/fields";
|
||||||
export { FormFieldRenderer } from "./src/components/fields/FormFieldRenderer";
|
export { FormFieldRenderer } from "./src/components/fields/FormFieldRenderer";
|
||||||
export { CurrencyField, formatCurrency } from "./src/components/fields/renderers/CurrencyField";
|
export { CurrencyField, formatCurrency } from "./src/components/fields/renderers/CurrencyField";
|
||||||
|
export { FkMultiSelectField } from "./src/components/fields/renderers/FkMultiSelectField";
|
||||||
export { SseStreamView } from "./src/components/SseStreamView";
|
export { SseStreamView } from "./src/components/SseStreamView";
|
||||||
export { SseConnectionStatus } from "./src/components/SseConnectionStatus";
|
export { SseConnectionStatus } from "./src/components/SseConnectionStatus";
|
||||||
export { getApi } from "./src/hooks/useApi";
|
export { getApi } from "./src/hooks/useApi";
|
||||||
|
|||||||
@@ -10,14 +10,42 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import CachedIcon from "@mui/icons-material/Cached";
|
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 { StatCard } from "../common/components/StatCard";
|
||||||
import { TransactionList } from "../common/components/TransactionList";
|
import { TransactionList } from "../common/components/TransactionList";
|
||||||
import { OptionMultiSelect } from "../common/components/OptionMultiSelect";
|
|
||||||
import type { TxnFieldConfigs } from "../common/types";
|
import type { TxnFieldConfigs } from "../common/types";
|
||||||
import { toPeriodGranularity } from "../common/utils/transactions";
|
import { toPeriodGranularity } from "../common/utils/transactions";
|
||||||
import { aggregateSlice } from "./types";
|
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 {
|
interface ReportViewerProps {
|
||||||
id: string;
|
id: string;
|
||||||
version: number;
|
version: number;
|
||||||
@@ -70,6 +98,15 @@ export function ReportViewer({ id, version, fields, onClose, onRegenerated }: Re
|
|||||||
};
|
};
|
||||||
}, [report]);
|
}, [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(
|
const slice = useMemo(
|
||||||
() => aggregateSlice(data ?? [], { periods: selectedPeriods, payees: selectedPayees }),
|
() => aggregateSlice(data ?? [], { periods: selectedPeriods, payees: selectedPayees }),
|
||||||
[data, selectedPeriods, selectedPayees],
|
[data, selectedPeriods, selectedPayees],
|
||||||
@@ -149,18 +186,22 @@ export function ReportViewer({ id, version, fields, onClose, onRegenerated }: Re
|
|||||||
) : (
|
) : (
|
||||||
<Box sx={{ p: 2.5 }}>
|
<Box sx={{ p: 2.5 }}>
|
||||||
<Box sx={{ display: "flex", gap: 1.5, flexWrap: "wrap", mb: 2.5 }}>
|
<Box sx={{ display: "flex", gap: 1.5, flexWrap: "wrap", mb: 2.5 }}>
|
||||||
<OptionMultiSelect
|
<Box sx={{ width: 260 }}>
|
||||||
label="Period"
|
<FkMultiSelectField
|
||||||
options={groupOptions.period}
|
field={{ ...periodField, readOnly: groupOptions.period.length === 0 }}
|
||||||
value={selectedPeriods}
|
fkOptions={periodOptions}
|
||||||
onChange={setSelectedPeriods}
|
value={selectedPeriods}
|
||||||
/>
|
onChange={setSelectedPeriods}
|
||||||
<OptionMultiSelect
|
/>
|
||||||
label="Payee"
|
</Box>
|
||||||
options={groupOptions.payee}
|
<Box sx={{ width: 260 }}>
|
||||||
value={selectedPayees}
|
<FkMultiSelectField
|
||||||
onChange={setSelectedPayees}
|
field={{ ...payeeField, readOnly: groupOptions.payee.length === 0 }}
|
||||||
/>
|
fkOptions={payeeOptions}
|
||||||
|
value={selectedPayees}
|
||||||
|
onChange={setSelectedPayees}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ alignSelf: "center" }}>
|
<Typography variant="body2" color="text.secondary" sx={{ alignSelf: "center" }}>
|
||||||
Showing {slice.count} transactions across {slice.txns.length} rows
|
Showing {slice.count} transactions across {slice.txns.length} rows
|
||||||
</Typography>
|
</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