report-fetch-request-ui #7

Merged
aetos merged 20 commits from report-fetch-request-ui into main 2026-05-24 17:23:03 +00:00
5 changed files with 101 additions and 93 deletions
Showing only changes of commit a3970d6a7b - Show all commits

View File

@@ -14,16 +14,6 @@ import {
import FilterListIcon from "@mui/icons-material/FilterList"; import FilterListIcon from "@mui/icons-material/FilterList";
import { ResourceField, ResourceMode } from "../types/config"; import { ResourceField, ResourceMode } from "../types/config";
function getDisplayValue(item: any, field: ResourceField): string {
if (!item) return "";
const df = field.displayField;
if (!df) return item.name || item.title || item.label || String(item.id ?? "");
if (Array.isArray(df)) {
return df.map((k) => item[k]).filter((v) => v != null).join(" ");
}
return item[df] ?? String(item.id ?? "");
}
function extractOptions( function extractOptions(
fieldName: string, fieldName: string,
field: ResourceField, field: ResourceField,
@@ -31,33 +21,45 @@ function extractOptions(
): string[] { ): string[] {
const values = new Set<string>(); const values = new Set<string>();
if (field.options) { if (field.options) return field.options;
return field.options;
}
if (!data) return []; if (!data) return [];
for (const item of data) { const pull = (item: any): string | null => {
const v = item[fieldName]; if (item == null) return null;
if (typeof item === "string") return item;
if (typeof item !== "object") return String(item);
const df = field.displayField;
if (!df) { debugger; return null; }
if (Array.isArray(df)) {
const parts = df.map((k) => item[k]).filter((v) => v != null);
if (parts.length > 0) return parts.join(" ");
} else {
const v = item[df];
if (v != null) return String(v);
}
debugger;
return null;
};
for (const row of data) {
const v = row[fieldName];
if (v == null) continue; if (v == null) continue;
if (field.type === "array" && Array.isArray(v)) { if (Array.isArray(v)) {
for (const el of v) { for (const el of v) {
if (el != null && typeof el === "object") { const label = pull(el);
const d = getDisplayValue(el, field); if (label) values.add(label);
if (d) values.add(d);
} else if (el != null) {
values.add(String(el));
} }
}
} else if (typeof v === "object") {
const d = getDisplayValue(v, field);
if (d) values.add(d);
} else { } else {
values.add(String(v)); const label = pull(v);
if (label) values.add(label);
} }
} }
console.log('extracted', fieldName, Array.from(values).sort())
return Array.from(values).sort(); return Array.from(values).sort();
} }
@@ -68,22 +70,44 @@ function renderFilterInput(
value: any, value: any,
onChange: (key: string, val: any) => void onChange: (key: string, val: any) => void
) { ) {
const isRange = const filterType = field.filterType;
field.type === "number" || field.type === "datetime" || field.type === "date";
if (isRange) { if (filterType === "number-range") {
const rangeVal = (value as { min?: string; max?: string; start?: string; end?: string }) || {}; const rangeVal = (value as { min?: string; max?: string }) || {};
const isDate = field.type === "datetime" || field.type === "date";
const inputType = isDate ? "datetime-local" : "number";
if (isDate) {
return ( return (
<Box key={fieldName} sx={{ display: "flex", gap: 1, alignItems: "center" }}> <Box key={fieldName} sx={{ display: "flex", gap: 1, alignItems: "center" }}>
<Typography variant="caption" sx={{ minWidth: 80, color: "text.secondary" }}> <Typography variant="caption" sx={{ minWidth: 80, color: "text.secondary" }}>
{field.label} {field.label}
</Typography> </Typography>
<TextField <TextField
type={inputType} type="number"
placeholder="Min"
size="small"
value={rangeVal.min ?? ""}
onChange={(e) => onChange("min", e.target.value || undefined)}
sx={{ width: 120 }}
/>
<TextField
type="number"
placeholder="Max"
size="small"
value={rangeVal.max ?? ""}
onChange={(e) => onChange("max", e.target.value || undefined)}
sx={{ width: 120 }}
/>
</Box>
);
}
if (filterType === "date-range") {
const rangeVal = (value as { start?: string; end?: string }) || {};
return (
<Box key={fieldName} sx={{ display: "flex", gap: 1, alignItems: "center" }}>
<Typography variant="caption" sx={{ minWidth: 80, color: "text.secondary" }}>
{field.label}
</Typography>
<TextField
type="datetime-local"
placeholder="From" placeholder="From"
size="small" size="small"
value={rangeVal.start ?? ""} value={rangeVal.start ?? ""}
@@ -92,7 +116,7 @@ function renderFilterInput(
sx={{ width: 190 }} sx={{ width: 190 }}
/> />
<TextField <TextField
type={inputType} type="datetime-local"
placeholder="To" placeholder="To"
size="small" size="small"
value={rangeVal.end ?? ""} value={rangeVal.end ?? ""}
@@ -104,49 +128,24 @@ function renderFilterInput(
); );
} }
if (filterType === "multiselect") {
const selected = Array.isArray(value) ? value : [];
return ( return (
<Box key={fieldName} sx={{ display: "flex", gap: 1, alignItems: "center" }}> <Autocomplete
<Typography variant="caption" sx={{ minWidth: 80, color: "text.secondary" }}> key={fieldName}
{field.label} multiple
</Typography> options={options}
<TextField value={selected}
type={inputType} onChange={(_, val) => onChange("value", val.length > 0 ? val : undefined)}
placeholder="Min" renderInput={(params) => (
<TextField {...params} label={field.label} size="small" />
)}
sx={{ minWidth: 220 }}
size="small" size="small"
value={rangeVal.min ?? ""}
onChange={(e) => onChange("min", e.target.value || undefined)}
sx={{ width: 120 }}
/> />
<TextField
type={inputType}
placeholder="Max"
size="small"
value={rangeVal.max ?? ""}
onChange={(e) => onChange("max", e.target.value || undefined)}
sx={{ width: 120 }}
/>
</Box>
); );
} }
if (field.type === "boolean") {
return (
<FormControl key={fieldName} size="small" sx={{ minWidth: 140 }}>
<InputLabel>{field.label}</InputLabel>
<Select
value={value ?? ""}
label={field.label}
onChange={(e) => onChange("value", e.target.value || undefined)}
>
<MenuItem value="">All</MenuItem>
<MenuItem value="true">Yes</MenuItem>
<MenuItem value="false">No</MenuItem>
</Select>
</FormControl>
);
}
if (options.length <= 20) {
return ( return (
<Autocomplete <Autocomplete
key={fieldName} key={fieldName}
@@ -162,18 +161,6 @@ function renderFilterInput(
); );
} }
return (
<TextField
key={fieldName}
label={field.label}
value={value ?? ""}
onChange={(e) => onChange("value", e.target.value || undefined)}
size="small"
sx={{ minWidth: 180 }}
/>
);
}
export interface FilterBarProps { export interface FilterBarProps {
fields: Record<string, ResourceField>; fields: Record<string, ResourceField>;
filterableFields: string[]; filterableFields: string[];

View File

@@ -16,7 +16,7 @@ interface ResourceViewProps {
import { GridPaginationModel } from '@mui/x-data-grid'; import { GridPaginationModel } from '@mui/x-data-grid';
function getFilterDisplayFields(field: ResourceField): string[] { function getFilterDisplayFields(field: ResourceField): string[] {
if (!field.displayField) return ["name", "title", "label"]; if (!field.displayField) return [];
return (Array.isArray(field.displayField) ? field.displayField : [field.displayField]).filter( return (Array.isArray(field.displayField) ? field.displayField : [field.displayField]).filter(
(df): df is string => !!df (df): df is string => !!df
); );
@@ -57,20 +57,32 @@ function applyClientFilters(
return true; return true;
} }
if (Array.isArray(filterValue)) {
if (itemValue && typeof itemValue === "object") {
const dispFields = getFilterDisplayFields(field);
const itemDisplay = dispFields.map((df) => itemValue[df]).filter((v) => v != null).join(" ");
return filterValue.includes(itemDisplay);
}
return filterValue.includes(String(itemValue));
}
if (!filterValue) return true; if (!filterValue) return true;
if (field.type === "boolean") { if (field.type === "boolean") {
return String(itemValue) === filterValue; return String(itemValue) === filterValue;
} }
if (field.type === "array" && Array.isArray(itemValue) && field.relation) { if (field.type === "array" && Array.isArray(itemValue)) {
return itemValue.some((el: any) => {
if (el != null && typeof el === "object") {
const dispFields = getFilterDisplayFields(field); const dispFields = getFilterDisplayFields(field);
return itemValue.some((el: any) => return dispFields.some((df) => String(el[df]) === String(filterValue));
dispFields.some((df) => String(el[df]) === String(filterValue)) }
); return String(el) === String(filterValue);
});
} }
if (field.relation && itemValue && typeof itemValue === "object") { if (itemValue && typeof itemValue === "object") {
const dispFields = getFilterDisplayFields(field); const dispFields = getFilterDisplayFields(field);
return dispFields.some((df) => String(itemValue[df]) === String(filterValue)); return dispFields.some((df) => String(itemValue[df]) === String(filterValue));
} }

View File

@@ -20,6 +20,7 @@ export interface ResourceField {
displayField?: string | string[]; displayField?: string | string[];
formatter?: (value: any) => string; formatter?: (value: any) => string;
relation?: string; // Name of the target resource relation?: string; // Name of the target resource
filterType?: "autocomplete" | "multiselect" | "number-range" | "date-range";
} }
export type ResourceMode = "server" | "client"; export type ResourceMode = "server" | "client";

View File

@@ -7,6 +7,7 @@ export interface FieldOverride {
displayField?: string | string[]; displayField?: string | string[];
display?: boolean; display?: boolean;
formatter?: (value: any) => string; formatter?: (value: any) => string;
filterType?: "autocomplete" | "multiselect" | "number-range" | "date-range";
} }
export interface ResourceOverride { export interface ResourceOverride {

View File

@@ -7,6 +7,7 @@ export const configuration: Record<string, ResourceOverride> = {
fields: { fields: {
payee: { payee: {
displayField: "name", displayField: "name",
filterType: "autocomplete",
}, },
payor: { payor: {
display: false, display: false,
@@ -14,11 +15,14 @@ export const configuration: Record<string, ResourceOverride> = {
}, },
account: { account: {
displayField: "name", displayField: "name",
filterType: "multiselect",
}, },
tags: { tags: {
displayField: ["name", "icon"], displayField: ["name", "icon"],
filterType: "autocomplete",
}, },
occurred_at: { occurred_at: {
filterType: "date-range",
formatter: (val: string) => { formatter: (val: string) => {
const date = new Date(val); const date = new Date(val);
const day = date.getDate(); const day = date.getDate();
@@ -36,6 +40,9 @@ export const configuration: Record<string, ResourceOverride> = {
return `${day}${suffix(day)} ${month} ${year}`; return `${day}${suffix(day)} ${month} ${year}`;
} }
}, },
amount: {
filterType: "number-range",
},
created_at: { created_at: {
display: false display: false
} }