navigation
This commit is contained in:
@@ -9,6 +9,14 @@ import { initializeApiClients } from "./api/client";
|
|||||||
import { AppConfig } from "./types/config";
|
import { AppConfig } from "./types/config";
|
||||||
import { Box, Typography, Paper, CircularProgress } from "@mui/material";
|
import { Box, Typography, Paper, CircularProgress } from "@mui/material";
|
||||||
import AppTheme from "../src/shared-theme/AppTheme";
|
import AppTheme from "../src/shared-theme/AppTheme";
|
||||||
|
import {
|
||||||
|
BrowserRouter,
|
||||||
|
Routes,
|
||||||
|
Route,
|
||||||
|
useNavigate,
|
||||||
|
useParams,
|
||||||
|
Navigate,
|
||||||
|
} from "react-router-dom";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -17,26 +25,38 @@ export const ConfigContext = React.createContext<AppConfig | null>(null);
|
|||||||
|
|
||||||
function Dashboard() {
|
function Dashboard() {
|
||||||
const config = React.useContext(ConfigContext);
|
const config = React.useContext(ConfigContext);
|
||||||
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
Welcome to the Admin Panel
|
Welcome to the Admin Panel
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1" sx={{ color: 'text.secondary' }}>
|
||||||
Select a resource from the sidebar to manage data.
|
Select a resource from the sidebar to manage data.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
|
gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))",
|
||||||
gap: 3,
|
gap: 3,
|
||||||
mt: 4,
|
mt: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{config?.resources.map((res) => (
|
{config?.resources.map((res) => (
|
||||||
<Paper key={res.name} sx={{ p: 3, textAlign: "center" }}>
|
<Paper
|
||||||
<Typography variant="h6">{res.pluralLabel}</Typography>
|
key={res.name}
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
textAlign: "center",
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
'&:hover': { transform: 'translateY(-4px)', boxShadow: 4 }
|
||||||
|
}}
|
||||||
|
onClick={() => navigate(`/${res.name}`)}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" color="primary">{res.pluralLabel}</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">Manage {res.pluralLabel.toLowerCase()}</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -47,15 +67,7 @@ function Dashboard() {
|
|||||||
function AdminApp() {
|
function AdminApp() {
|
||||||
const { currentUser, login, logout, loading, error } = useAuth();
|
const { currentUser, login, logout, loading, error } = useAuth();
|
||||||
const config = React.useContext(ConfigContext);
|
const config = React.useContext(ConfigContext);
|
||||||
const [selectedResourceName, setSelectedResourceName] = React.useState<
|
const navigate = useNavigate();
|
||||||
string | null
|
|
||||||
>(null);
|
|
||||||
const [selectedItemId, setSelectedItemId] = React.useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleNavigateToResource = (resourceName: string, id: string) => {
|
|
||||||
setSelectedResourceName(resourceName);
|
|
||||||
setSelectedItemId(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
return (
|
return (
|
||||||
@@ -72,34 +84,34 @@ function AdminApp() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedResource = config?.resources.find(
|
|
||||||
(r) => r.name === selectedResourceName
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout
|
<AdminLayout
|
||||||
username={currentUser.username}
|
username={currentUser.username}
|
||||||
onLogout={logout}
|
onLogout={logout}
|
||||||
selectedResourceName={selectedResourceName}
|
onSelectResource={(name) => navigate(`/${name}`)}
|
||||||
onSelectResource={(name) => {
|
|
||||||
setSelectedResourceName(name);
|
|
||||||
setSelectedItemId(null);
|
|
||||||
}}
|
|
||||||
resources={config?.resources || []}
|
resources={config?.resources || []}
|
||||||
>
|
>
|
||||||
{selectedResource ? (
|
<Routes>
|
||||||
<ResourceView
|
<Route path="/" element={<Dashboard />} />
|
||||||
key={`${selectedResource.name}-${selectedItemId}`}
|
<Route path="/:resourceName" element={<ResourceRouteWrapper />} />
|
||||||
config={selectedResource}
|
<Route path="/:resourceName/:id" element={<ResourceRouteWrapper />} />
|
||||||
onNavigateToResource={handleNavigateToResource}
|
<Route path="/:resourceName/create" element={<ResourceRouteWrapper />} />
|
||||||
/>
|
<Route path="/:resourceName/edit/:id" element={<ResourceRouteWrapper />} />
|
||||||
) : (
|
</Routes>
|
||||||
<Dashboard />
|
|
||||||
)}
|
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ResourceRouteWrapper() {
|
||||||
|
const { resourceName } = useParams();
|
||||||
|
const config = React.useContext(ConfigContext);
|
||||||
|
const selectedResource = config?.resources.find((r) => r.name === resourceName);
|
||||||
|
|
||||||
|
if (!selectedResource) return <Typography>Resource not found</Typography>;
|
||||||
|
|
||||||
|
return <ResourceView config={selectedResource} />;
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [config, setConfig] = React.useState<AppConfig | null>(null);
|
const [config, setConfig] = React.useState<AppConfig | null>(null);
|
||||||
|
|
||||||
@@ -133,7 +145,9 @@ export default function App() {
|
|||||||
<ConfigContext.Provider value={config}>
|
<ConfigContext.Provider value={config}>
|
||||||
<AuthProvider authBaseUrl={config.authBaseUrl}>
|
<AuthProvider authBaseUrl={config.authBaseUrl}>
|
||||||
<UploadProvider>
|
<UploadProvider>
|
||||||
<AdminApp />
|
<BrowserRouter>
|
||||||
|
<AdminApp />
|
||||||
|
</BrowserRouter>
|
||||||
</UploadProvider>
|
</UploadProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ConfigContext.Provider>
|
</ConfigContext.Provider>
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ import TableViewIcon from '@mui/icons-material/TableView';
|
|||||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||||
import LogoutIcon from '@mui/icons-material/Logout';
|
import LogoutIcon from '@mui/icons-material/Logout';
|
||||||
import { ResourceConfig } from '../types/config';
|
import { ResourceConfig } from '../types/config';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const drawerWidth = 240;
|
const drawerWidth = 240;
|
||||||
|
|
||||||
interface AdminLayoutProps {
|
interface AdminLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onSelectResource: (resourceName: string | null) => void;
|
onSelectResource: (resourceName: string | null) => void;
|
||||||
selectedResourceName: string | null;
|
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
username?: string;
|
username?: string;
|
||||||
resources: ResourceConfig[];
|
resources: ResourceConfig[];
|
||||||
@@ -33,11 +33,14 @@ interface AdminLayoutProps {
|
|||||||
export default function AdminLayout({
|
export default function AdminLayout({
|
||||||
children,
|
children,
|
||||||
onSelectResource,
|
onSelectResource,
|
||||||
selectedResourceName,
|
|
||||||
onLogout,
|
onLogout,
|
||||||
username,
|
username,
|
||||||
resources,
|
resources,
|
||||||
}: AdminLayoutProps) {
|
}: AdminLayoutProps) {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const activeResourceName = location.pathname.split('/')[1] || null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
@@ -67,8 +70,8 @@ export default function AdminLayout({
|
|||||||
<List>
|
<List>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
selected={selectedResourceName === null}
|
selected={location.pathname === '/'}
|
||||||
onClick={() => onSelectResource(null)}
|
onClick={() => navigate('/')}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DashboardIcon />
|
<DashboardIcon />
|
||||||
@@ -82,7 +85,7 @@ export default function AdminLayout({
|
|||||||
{resources.map((res) => (
|
{resources.map((res) => (
|
||||||
<ListItem key={res.name} disablePadding>
|
<ListItem key={res.name} disablePadding>
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
selected={selectedResourceName === res.name}
|
selected={activeResourceName === res.name}
|
||||||
onClick={() => onSelectResource(res.name)}
|
onClick={() => onSelectResource(res.name)}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import {
|
|||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import { ResourceConfig, ResourceField } from '../types/config';
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { ResourceConfig } from '../types/config';
|
||||||
|
|
||||||
interface EnhancedTableProps {
|
interface EnhancedTableProps {
|
||||||
config: ResourceConfig;
|
config: ResourceConfig;
|
||||||
@@ -34,6 +36,7 @@ export default function EnhancedTable({
|
|||||||
onCreate,
|
onCreate,
|
||||||
onNavigateToResource,
|
onNavigateToResource,
|
||||||
}: EnhancedTableProps) {
|
}: EnhancedTableProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const columns: GridColDef[] = React.useMemo(() => {
|
const columns: GridColDef[] = React.useMemo(() => {
|
||||||
const cols: GridColDef[] = Object.entries(config.fields).map(([key, field]) => {
|
const cols: GridColDef[] = Object.entries(config.fields).map(([key, field]) => {
|
||||||
@@ -45,6 +48,9 @@ export default function EnhancedTable({
|
|||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
const value = params.value;
|
const value = params.value;
|
||||||
|
|
||||||
|
// 0. Link to View if it's the Primary Key (if it's displayed)
|
||||||
|
const isPk = key === config.primaryKey;
|
||||||
|
|
||||||
// 1. Custom Formatter
|
// 1. Custom Formatter
|
||||||
if (field.formatter) {
|
if (field.formatter) {
|
||||||
return field.formatter(value);
|
return field.formatter(value);
|
||||||
@@ -99,7 +105,23 @@ export default function EnhancedTable({
|
|||||||
// 4. Default renderings
|
// 4. Default renderings
|
||||||
if (field.type === 'boolean') return value ? 'Yes' : 'No';
|
if (field.type === 'boolean') return value ? 'Yes' : 'No';
|
||||||
if (field.type === 'datetime' || field.type === 'date') {
|
if (field.type === 'datetime' || field.type === 'date') {
|
||||||
return new Date(value).toLocaleString();
|
return (value) ? new Date(value).toLocaleString() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPk) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
variant="body2"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const rowId = params.row[config.primaryKey];
|
||||||
|
navigate(`/${config.name}/${rowId}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
@@ -112,12 +134,17 @@ export default function EnhancedTable({
|
|||||||
field: 'actions',
|
field: 'actions',
|
||||||
type: 'actions',
|
type: 'actions',
|
||||||
headerName: 'Actions',
|
headerName: 'Actions',
|
||||||
width: 100,
|
width: 120,
|
||||||
getActions: (params) => [
|
getActions: (params) => [
|
||||||
|
<GridActionsCellItem
|
||||||
|
icon={<VisibilityIcon />}
|
||||||
|
label="View"
|
||||||
|
onClick={() => navigate(`/${config.name}/${params.id}`)}
|
||||||
|
/>,
|
||||||
<GridActionsCellItem
|
<GridActionsCellItem
|
||||||
icon={<EditIcon />}
|
icon={<EditIcon />}
|
||||||
label="Edit"
|
label="Edit"
|
||||||
onClick={() => onEdit(params.row)}
|
onClick={() => navigate(`/${config.name}/edit/${params.id}`)}
|
||||||
/>,
|
/>,
|
||||||
<GridActionsCellItem
|
<GridActionsCellItem
|
||||||
icon={<DeleteIcon />}
|
icon={<DeleteIcon />}
|
||||||
@@ -128,7 +155,7 @@ export default function EnhancedTable({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return cols;
|
return cols;
|
||||||
}, [config, onEdit, onDelete, onNavigateToResource]);
|
}, [config, onDelete, navigate, onNavigateToResource]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ height: 600, width: '100%' }}>
|
<Box sx={{ height: 600, width: '100%' }}>
|
||||||
@@ -144,14 +171,10 @@ export default function EnhancedTable({
|
|||||||
getRowId={(row) => {
|
getRowId={(row) => {
|
||||||
const pk = config.primaryKey;
|
const pk = config.primaryKey;
|
||||||
if (row[pk] !== undefined && row[pk] !== null) return row[pk];
|
if (row[pk] !== undefined && row[pk] !== null) return row[pk];
|
||||||
// Fallback: search for common ID fields
|
const fallbackKeys = ['id', '_id', 'uuid', 'pk'];
|
||||||
const fallbackKeys = ['id', 'uuid', 'pk'];
|
|
||||||
for (const key of fallbackKeys) {
|
for (const key of fallbackKeys) {
|
||||||
if (row[key] !== undefined && row[key] !== null) return row[key];
|
if (row[key] !== undefined && row[key] !== null) return row[key];
|
||||||
}
|
}
|
||||||
debugger;
|
|
||||||
|
|
||||||
// Absolute fallback: index (not ideal but avoids crash)
|
|
||||||
return `temp-id-${data.indexOf(row)}`;
|
return `temp-id-${data.indexOf(row)}`;
|
||||||
}}
|
}}
|
||||||
disableRowSelectionOnClick
|
disableRowSelectionOnClick
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ interface GenericFormProps {
|
|||||||
onSave: (data: any) => Promise<void>;
|
onSave: (data: any) => Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
onEditClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { ConfigContext } from '../App';
|
import { ConfigContext } from '../App';
|
||||||
@@ -32,6 +34,8 @@ export default function GenericForm({
|
|||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
loading: saving,
|
loading: saving,
|
||||||
|
readOnly = false,
|
||||||
|
onEditClick,
|
||||||
}: GenericFormProps) {
|
}: GenericFormProps) {
|
||||||
initialData = initialData || {};
|
initialData = initialData || {};
|
||||||
const [formData, setFormData] = React.useState(initialData);
|
const [formData, setFormData] = React.useState(initialData);
|
||||||
@@ -39,18 +43,25 @@ export default function GenericForm({
|
|||||||
const appConfig = React.useContext(ConfigContext);
|
const appConfig = React.useContext(ConfigContext);
|
||||||
|
|
||||||
const handleChange = (key: string, value: any) => {
|
const handleChange = (key: string, value: any) => {
|
||||||
|
if (readOnly) return;
|
||||||
setFormData((prev: any) => ({ ...prev, [key]: value }));
|
setFormData((prev: any) => ({ ...prev, [key]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (readOnly) return;
|
||||||
onSave(formData);
|
onSave(formData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTitle = () => {
|
||||||
|
if (readOnly) return `View ${config.label}`;
|
||||||
|
return initialData[config.primaryKey] ? `Edit ${config.label}` : `New ${config.label}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
<Typography variant="h5">
|
<Typography variant="h5">
|
||||||
{initialData[config.primaryKey] ? `Edit ${config.label}` : `New ${config.label}`}
|
{getTitle()}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
@@ -61,7 +72,7 @@ export default function GenericForm({
|
|||||||
field={field}
|
field={field}
|
||||||
value={formData[key]}
|
value={formData[key]}
|
||||||
onChange={(val: any) => handleChange(key, val)}
|
onChange={(val: any) => handleChange(key, val)}
|
||||||
disabled={field.readOnly}
|
disabled={readOnly || field.readOnly}
|
||||||
uploadFile={uploadFile}
|
uploadFile={uploadFile}
|
||||||
uploading={uploading}
|
uploading={uploading}
|
||||||
baseUrl={appConfig?.baseUrl || ""}
|
baseUrl={appConfig?.baseUrl || ""}
|
||||||
@@ -70,11 +81,17 @@ export default function GenericForm({
|
|||||||
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 4 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 4 }}>
|
||||||
<Button variant="outlined" onClick={onCancel} disabled={saving}>
|
<Button variant="outlined" onClick={onCancel} disabled={saving}>
|
||||||
Cancel
|
{readOnly ? 'Back to List' : 'Cancel'}
|
||||||
</Button>
|
|
||||||
<Button variant="contained" type="submit" loading={saving} disabled={saving || uploading}>
|
|
||||||
Save {config.label}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
{readOnly ? (
|
||||||
|
<Button variant="contained" color="primary" onClick={onEditClick}>
|
||||||
|
Edit {config.label}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="contained" type="submit" loading={saving} disabled={saving || uploading}>
|
||||||
|
Save {config.label}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ResourceConfig } from '../types/config';
|
|||||||
import { useResource } from '../hooks/useResource';
|
import { useResource } from '../hooks/useResource';
|
||||||
import GenericForm from './GenericForm';
|
import GenericForm from './GenericForm';
|
||||||
import EnhancedTable from './EnhancedTable';
|
import EnhancedTable from './EnhancedTable';
|
||||||
|
import { useParams, useLocation, useNavigate, Routes, Route } from 'react-router-dom';
|
||||||
|
|
||||||
interface ResourceViewProps {
|
interface ResourceViewProps {
|
||||||
config: ResourceConfig;
|
config: ResourceConfig;
|
||||||
@@ -11,68 +12,75 @@ interface ResourceViewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ResourceView({ config, onNavigateToResource }: ResourceViewProps) {
|
export default function ResourceView({ config, onNavigateToResource }: ResourceViewProps) {
|
||||||
const [view, setView] = React.useState<'list' | 'create' | 'edit'>('list');
|
const { id } = useParams();
|
||||||
const [selectedItem, setSelectedItem] = React.useState<any>(null);
|
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 { useList, useCreate, useUpdate, useDelete } = useResource(config);
|
const { useList, useRead, useCreate, useUpdate, useDelete } = useResource(config);
|
||||||
|
|
||||||
const { data, isLoading, error } = useList();
|
const listQuery = useList();
|
||||||
|
const itemQuery = useRead(id || "");
|
||||||
|
|
||||||
const createMutation = useCreate();
|
const createMutation = useCreate();
|
||||||
const updateMutation = useUpdate();
|
const updateMutation = useUpdate();
|
||||||
const deleteMutation = useDelete();
|
const deleteMutation = useDelete();
|
||||||
|
|
||||||
const handleEdit = (item: any) => {
|
const handleEdit = (item: any) => {
|
||||||
setSelectedItem(item);
|
navigate(`/${config.name}/edit/${item[config.primaryKey]}`);
|
||||||
setView('edit');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
setSelectedItem(null);
|
navigate(`/${config.name}/create`);
|
||||||
setView('create');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async (formData: any) => {
|
const handleSave = async (formData: any) => {
|
||||||
try {
|
try {
|
||||||
if (view === 'edit') {
|
if (isEdit) {
|
||||||
const id = formData[config.primaryKey];
|
await updateMutation.mutateAsync({ id: id!, data: formData });
|
||||||
await updateMutation.mutateAsync({ id, data: formData });
|
|
||||||
} else {
|
} else {
|
||||||
await createMutation.mutateAsync(formData);
|
await createMutation.mutateAsync(formData);
|
||||||
}
|
}
|
||||||
setView('list');
|
navigate(`/${config.name}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Save failed:', err);
|
console.error('Save failed:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (itemId: string) => {
|
||||||
if (window.confirm('Are you sure you want to delete this item?')) {
|
if (window.confirm('Are you sure you want to delete this item?')) {
|
||||||
await deleteMutation.mutateAsync(id);
|
await deleteMutation.mutateAsync(itemId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) return <CircularProgress />;
|
if (isList && listQuery.isLoading) return <CircularProgress />;
|
||||||
if (error) return <Typography color="error">Error loading {config.pluralLabel}</Typography>;
|
if ((isEdit || isView) && itemQuery.isLoading) return <CircularProgress />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{view === 'list' ? (
|
{isList ? (
|
||||||
<EnhancedTable
|
<EnhancedTable
|
||||||
config={config}
|
config={config}
|
||||||
data={data || []}
|
data={listQuery.data || []}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onCreate={handleCreate}
|
onCreate={handleCreate}
|
||||||
onNavigateToResource={onNavigateToResource}
|
onNavigateToResource={(res, id) => navigate(`/${res}/${id}`)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Paper sx={{ p: 4 }}>
|
<Paper sx={{ p: 4 }}>
|
||||||
<GenericForm
|
<GenericForm
|
||||||
config={config}
|
config={config}
|
||||||
initialData={selectedItem}
|
initialData={isCreate ? null : itemQuery.data}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onCancel={() => setView('list')}
|
onCancel={() => navigate(`/${config.name}`)}
|
||||||
loading={createMutation.isPending || updateMutation.isPending}
|
loading={createMutation.isPending || updateMutation.isPending}
|
||||||
|
readOnly={isView}
|
||||||
|
onEditClick={() => navigate(`/${config.name}/edit/${id}`)}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface ImageUploadFieldProps {
|
|||||||
onUpload: (file: File) => void;
|
onUpload: (file: File) => void;
|
||||||
size?: number;
|
size?: number;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ImageUploadField({
|
export default function ImageUploadField({
|
||||||
@@ -16,6 +17,7 @@ export default function ImageUploadField({
|
|||||||
onUpload,
|
onUpload,
|
||||||
size = 64,
|
size = 64,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
disabled = false,
|
||||||
}: ImageUploadFieldProps) {
|
}: ImageUploadFieldProps) {
|
||||||
|
|
||||||
const imgSrc = value
|
const imgSrc = value
|
||||||
@@ -33,23 +35,25 @@ export default function ImageUploadField({
|
|||||||
sx={{ width: size, height: size, borderRadius: 2 }}
|
sx={{ width: size, height: size, borderRadius: 2 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
{!disabled && (
|
||||||
variant="outlined"
|
<Button
|
||||||
component="label"
|
variant="outlined"
|
||||||
disabled={uploading}
|
component="label"
|
||||||
startIcon={uploading && <CircularProgress size={16} />}
|
disabled={uploading}
|
||||||
>
|
startIcon={uploading && <CircularProgress size={16} />}
|
||||||
{uploading ? "Uploading..." : "Choose File"}
|
>
|
||||||
<input
|
{uploading ? "Uploading..." : "Choose File"}
|
||||||
type="file"
|
<input
|
||||||
accept="image/*"
|
type="file"
|
||||||
hidden
|
accept="image/*"
|
||||||
onChange={(e) => {
|
hidden
|
||||||
const file = e.target.files?.[0];
|
onChange={(e) => {
|
||||||
if (file) onUpload(file);
|
const file = e.target.files?.[0];
|
||||||
}}
|
if (file) onUpload(file);
|
||||||
/>
|
}}
|
||||||
</Button>
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function useResource<T = any>(config: ResourceConfig) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- READ ONE ---
|
// --- READ ONE ---
|
||||||
const useOne = (id: string | null) =>
|
const useRead = (id: string | null) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: [name, "detail", id],
|
queryKey: [name, "detail", id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -69,7 +69,7 @@ export function useResource<T = any>(config: ResourceConfig) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
useList,
|
useList,
|
||||||
useOne,
|
useRead,
|
||||||
useCreate,
|
useCreate,
|
||||||
useUpdate,
|
useUpdate,
|
||||||
useDelete,
|
useDelete,
|
||||||
|
|||||||
Reference in New Issue
Block a user