218 lines
7.5 KiB
TypeScript
218 lines
7.5 KiB
TypeScript
import * as React from 'react';
|
|
import { Box, Paper, CircularProgress } from '@mui/material';
|
|
import { ResourceConfig } from '../types/config';
|
|
import type { ResourceField } from '../types/config';
|
|
import { FieldComponents } from '../types/overrides';
|
|
import { useResource } from '../hooks/useResource';
|
|
import { resolveTemplate } from '../utils/options';
|
|
import EnhancedTable from './EnhancedTable';
|
|
import FilterBar from './FilterBar';
|
|
import { useParams, useLocation, useNavigate } from 'react-router-dom';
|
|
|
|
interface ResourceViewProps {
|
|
config: ResourceConfig;
|
|
onNavigateToResource?: (resourceName: string, id: string) => void;
|
|
fieldComponents: FieldComponents;
|
|
}
|
|
|
|
import { GridPaginationModel } from '@mui/x-data-grid';
|
|
|
|
function getDisplayString(item: any, field: ResourceField): string {
|
|
if (item == null || typeof item !== 'object') return String(item ?? '');
|
|
if (field.enumOption?.value) return resolveTemplate(field.enumOption.value, item);
|
|
const df = field.displayField;
|
|
if (!df) return item.name ?? item.title ?? item.label ?? item.id ?? JSON.stringify(item);
|
|
if (Array.isArray(df)) {
|
|
const parts = df.map((k: string) => item[k]).filter((v: any) => v != null);
|
|
return parts.length > 0 ? parts.join(' ') : '';
|
|
}
|
|
return String(item[df] ?? '');
|
|
}
|
|
|
|
function applyClientFilters(
|
|
data: any[],
|
|
filters: Record<string, any>,
|
|
fields: Record<string, ResourceField>
|
|
): any[] {
|
|
const entries = Object.entries(filters).filter(([_, v]) => {
|
|
if (v == null || v === "" || (Array.isArray(v) && v.length === 0)) return false;
|
|
if (typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => x == null || x === "")) return false;
|
|
return true;
|
|
});
|
|
|
|
if (entries.length === 0) return data;
|
|
|
|
return data.filter((item) =>
|
|
entries.every(([fieldName, filterValue]) => {
|
|
const field = fields[fieldName];
|
|
if (!field) return true;
|
|
|
|
const itemValue = item[fieldName];
|
|
|
|
if (typeof filterValue === "object" && !Array.isArray(filterValue)) {
|
|
if (field.type === "number") {
|
|
if (filterValue.min != null && filterValue.min !== "" && Number(itemValue) < Number(filterValue.min)) return false;
|
|
if (filterValue.max != null && filterValue.max !== "" && Number(itemValue) > Number(filterValue.max)) return false;
|
|
return true;
|
|
}
|
|
if (field.type === "datetime" || field.type === "date") {
|
|
const itemTime = new Date(itemValue).getTime();
|
|
if (filterValue.start && new Date(filterValue.start).getTime() > itemTime) return false;
|
|
if (filterValue.end && new Date(filterValue.end).getTime() < itemTime) return false;
|
|
return true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (Array.isArray(filterValue)) {
|
|
if (field.type === "array" && Array.isArray(itemValue)) {
|
|
return itemValue.some((el: any) =>
|
|
filterValue.includes(getDisplayString(el, field))
|
|
);
|
|
}
|
|
if (itemValue && typeof itemValue === "object") {
|
|
return filterValue.includes(getDisplayString(itemValue, field));
|
|
}
|
|
return filterValue.includes(String(itemValue));
|
|
}
|
|
|
|
if (!filterValue) return true;
|
|
|
|
if (field.type === "boolean") {
|
|
return String(itemValue) === filterValue;
|
|
}
|
|
|
|
if (field.type === "array" && Array.isArray(itemValue)) {
|
|
return itemValue.some((el: any) =>
|
|
getDisplayString(el, field) === String(filterValue)
|
|
);
|
|
}
|
|
|
|
if (itemValue && typeof itemValue === "object") {
|
|
return getDisplayString(itemValue, field) === String(filterValue);
|
|
}
|
|
|
|
return String(itemValue) === String(filterValue);
|
|
})
|
|
);
|
|
}
|
|
|
|
export default function ResourceView({ config, onNavigateToResource, fieldComponents }: ResourceViewProps) {
|
|
const { id } = useParams();
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
|
|
const isCreate = location.pathname.endsWith('/create');
|
|
const isEdit = location.pathname.includes('/edit/');
|
|
const isView = !!id && !isEdit;
|
|
const isList = !id && !isCreate;
|
|
|
|
const isServer = config.filterOptions?.mode !== "client";
|
|
|
|
const [paginationModel, setPaginationModel] = React.useState<GridPaginationModel>({
|
|
page: 0,
|
|
pageSize: 10,
|
|
});
|
|
|
|
const [appliedFilters, setAppliedFilters] = React.useState<Record<string, any>>({});
|
|
|
|
const { useList, useRead, useCreate, useUpdate, useDelete, components } = useResource(config, { fieldComponents });
|
|
|
|
const queryParams = React.useMemo(() => {
|
|
if (!isServer) return { limit: 10 };
|
|
return {
|
|
skip: paginationModel.page * paginationModel.pageSize,
|
|
limit: paginationModel.pageSize,
|
|
};
|
|
}, [isServer, paginationModel]);
|
|
|
|
const listQuery = useList(queryParams);
|
|
const itemQuery = useRead(id || "");
|
|
|
|
const rawData = listQuery.data?.data || [];
|
|
const totalCount = listQuery.data?.total;
|
|
|
|
const filteredData = React.useMemo(
|
|
() => (isServer ? rawData : applyClientFilters(rawData, appliedFilters, config.fields)),
|
|
[isServer, rawData, appliedFilters, config.fields]
|
|
);
|
|
|
|
const createMutation = useCreate();
|
|
const updateMutation = useUpdate();
|
|
const deleteMutation = useDelete();
|
|
|
|
const handleEdit = (item: any) => {
|
|
navigate(`/admin/${config.name}/edit/${item[config.primaryKey]}`);
|
|
};
|
|
|
|
const handleCreate = () => {
|
|
navigate(`/admin/${config.name}/create`);
|
|
};
|
|
|
|
const handleSave = async (formData: any) => {
|
|
try {
|
|
if (isEdit) {
|
|
await updateMutation.mutateAsync({ id: id!, data: formData });
|
|
} else {
|
|
await createMutation.mutateAsync(formData);
|
|
}
|
|
navigate(`/admin/${config.name}`);
|
|
} catch (err) {
|
|
console.error('Save failed:', err);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (itemId: string) => {
|
|
if (window.confirm('Are you sure you want to delete this item?')) {
|
|
await deleteMutation.mutateAsync(itemId);
|
|
}
|
|
};
|
|
|
|
if (isList && listQuery.isLoading) return <CircularProgress />;
|
|
if ((isEdit || isView) && itemQuery.isLoading) return <CircularProgress />;
|
|
|
|
return (
|
|
<Box>
|
|
{isList ? (
|
|
<Box>
|
|
{!isServer && config.filterOptions?.fields && config.filterOptions.fields.length > 0 && (
|
|
<FilterBar
|
|
fields={config.fields}
|
|
filterableFields={config.filterOptions.fields}
|
|
mode={config.filterOptions?.mode || "server"}
|
|
data={rawData}
|
|
appliedValues={appliedFilters}
|
|
onApply={setAppliedFilters}
|
|
onClear={() => setAppliedFilters({})}
|
|
/>
|
|
)}
|
|
<EnhancedTable
|
|
config={config}
|
|
data={filteredData}
|
|
total={isServer ? totalCount : filteredData.length}
|
|
paginationModel={isServer ? paginationModel : undefined}
|
|
onPaginationModelChange={isServer ? setPaginationModel : undefined}
|
|
loading={listQuery.isFetching}
|
|
onEdit={handleEdit}
|
|
onDelete={handleDelete}
|
|
onCreate={handleCreate}
|
|
onNavigateToResource={(res, id) => navigate(`/admin/${res}/${id}`)}
|
|
/>
|
|
</Box>
|
|
) : (
|
|
<Paper sx={{ p: 4 }}>
|
|
{components && <components.GenericForm
|
|
config={config}
|
|
initialData={isCreate ? null : itemQuery.data}
|
|
onSave={handleSave}
|
|
onCancel={() => navigate(`/admin/${config.name}`)}
|
|
loading={createMutation.isPending || updateMutation.isPending}
|
|
readOnly={isView}
|
|
onEditClick={() => navigate(`/admin/${config.name}/edit/${id}`)}
|
|
/>}
|
|
</Paper>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|