filter in config and fixes

This commit is contained in:
2026-05-24 01:21:51 +05:30
parent 7ab5ce74b3
commit a3970d6a7b
5 changed files with 101 additions and 93 deletions

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,49 +70,17 @@ 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) {
const rangeVal = (value as { min?: string; max?: string; start?: string; end?: string }) || {};
const isDate = field.type === "datetime" || field.type === "date";
const inputType = isDate ? "datetime-local" : "number";
if (isDate) {
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={inputType}
placeholder="From"
size="small"
value={rangeVal.start ?? ""}
onChange={(e) => onChange("start", e.target.value || undefined)}
InputLabelProps={{ shrink: true }}
sx={{ width: 190 }}
/>
<TextField
type={inputType}
placeholder="To"
size="small"
value={rangeVal.end ?? ""}
onChange={(e) => onChange("end", e.target.value || undefined)}
InputLabelProps={{ shrink: true }}
sx={{ width: 190 }}
/>
</Box>
);
}
if (filterType === "number-range") {
const rangeVal = (value as { min?: string; max?: string }) || {};
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" placeholder="Min"
size="small" size="small"
value={rangeVal.min ?? ""} value={rangeVal.min ?? ""}
@@ -118,7 +88,7 @@ function renderFilterInput(
sx={{ width: 120 }} sx={{ width: 120 }}
/> />
<TextField <TextField
type={inputType} type="number"
placeholder="Max" placeholder="Max"
size="small" size="small"
value={rangeVal.max ?? ""} value={rangeVal.max ?? ""}
@@ -129,47 +99,64 @@ function renderFilterInput(
); );
} }
if (field.type === "boolean") { if (filterType === "date-range") {
const rangeVal = (value as { start?: string; end?: string }) || {};
return ( return (
<FormControl key={fieldName} size="small" sx={{ minWidth: 140 }}> <Box key={fieldName} sx={{ display: "flex", gap: 1, alignItems: "center" }}>
<InputLabel>{field.label}</InputLabel> <Typography variant="caption" sx={{ minWidth: 80, color: "text.secondary" }}>
<Select {field.label}
value={value ?? ""} </Typography>
label={field.label} <TextField
onChange={(e) => onChange("value", e.target.value || undefined)} type="datetime-local"
> placeholder="From"
<MenuItem value="">All</MenuItem> size="small"
<MenuItem value="true">Yes</MenuItem> value={rangeVal.start ?? ""}
<MenuItem value="false">No</MenuItem> onChange={(e) => onChange("start", e.target.value || undefined)}
</Select> InputLabelProps={{ shrink: true }}
</FormControl> sx={{ width: 190 }}
/>
<TextField
type="datetime-local"
placeholder="To"
size="small"
value={rangeVal.end ?? ""}
onChange={(e) => onChange("end", e.target.value || undefined)}
InputLabelProps={{ shrink: true }}
sx={{ width: 190 }}
/>
</Box>
); );
} }
if (options.length <= 20) { if (filterType === "multiselect") {
const selected = Array.isArray(value) ? value : [];
return ( return (
<Autocomplete <Autocomplete
key={fieldName} key={fieldName}
multiple
options={options} options={options}
value={value ?? null} value={selected}
onChange={(_, val) => onChange("value", val || undefined)} onChange={(_, val) => onChange("value", val.length > 0 ? val : undefined)}
renderInput={(params) => ( renderInput={(params) => (
<TextField {...params} label={field.label} size="small" /> <TextField {...params} label={field.label} size="small" />
)} )}
sx={{ minWidth: 180 }} sx={{ minWidth: 220 }}
size="small" size="small"
/> />
); );
} }
return ( return (
<TextField <Autocomplete
key={fieldName} key={fieldName}
label={field.label} options={options}
value={value ?? ""} value={value ?? null}
onChange={(e) => onChange("value", e.target.value || undefined)} onChange={(_, val) => onChange("value", val || undefined)}
size="small" renderInput={(params) => (
<TextField {...params} label={field.label} size="small" />
)}
sx={{ minWidth: 180 }} sx={{ minWidth: 180 }}
size="small"
/> />
); );
} }

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)) {
const dispFields = getFilterDisplayFields(field); return itemValue.some((el: any) => {
return itemValue.some((el: any) => if (el != null && typeof el === "object") {
dispFields.some((df) => String(el[df]) === String(filterValue)) const dispFields = getFilterDisplayFields(field);
); return 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
} }