filter in config and fixes
This commit is contained in:
@@ -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"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user