diff --git a/src_generic/App.tsx b/src_generic/App.tsx index c31f1bf..076730c 100644 --- a/src_generic/App.tsx +++ b/src_generic/App.tsx @@ -9,6 +9,14 @@ import { initializeApiClients } from "./api/client"; import { AppConfig } from "./types/config"; import { Box, Typography, Paper, CircularProgress } from "@mui/material"; import AppTheme from "../src/shared-theme/AppTheme"; +import { + BrowserRouter, + Routes, + Route, + useNavigate, + useParams, + Navigate, +} from "react-router-dom"; const queryClient = new QueryClient(); @@ -17,26 +25,38 @@ export const ConfigContext = React.createContext(null); function Dashboard() { const config = React.useContext(ConfigContext); + const navigate = useNavigate(); return ( Welcome to the Admin Panel - + Select a resource from the sidebar to manage data. {config?.resources.map((res) => ( - - {res.pluralLabel} + navigate(`/${res.name}`)} + > + {res.pluralLabel} + Manage {res.pluralLabel.toLowerCase()} ))} @@ -47,15 +67,7 @@ function Dashboard() { function AdminApp() { const { currentUser, login, logout, loading, error } = useAuth(); const config = React.useContext(ConfigContext); - const [selectedResourceName, setSelectedResourceName] = React.useState< - string | null - >(null); - const [selectedItemId, setSelectedItemId] = React.useState(null); - - const handleNavigateToResource = (resourceName: string, id: string) => { - setSelectedResourceName(resourceName); - setSelectedItemId(id); - }; + const navigate = useNavigate(); if (!currentUser) { return ( @@ -72,34 +84,34 @@ function AdminApp() { ); } - const selectedResource = config?.resources.find( - (r) => r.name === selectedResourceName - ); - return ( { - setSelectedResourceName(name); - setSelectedItemId(null); - }} + onSelectResource={(name) => navigate(`/${name}`)} resources={config?.resources || []} > - {selectedResource ? ( - - ) : ( - - )} + + } /> + } /> + } /> + } /> + } /> + ); } +function ResourceRouteWrapper() { + const { resourceName } = useParams(); + const config = React.useContext(ConfigContext); + const selectedResource = config?.resources.find((r) => r.name === resourceName); + + if (!selectedResource) return Resource not found; + + return ; +} + export default function App() { const [config, setConfig] = React.useState(null); @@ -133,7 +145,9 @@ export default function App() { - + + + diff --git a/src_generic/components/AdminLayout.tsx b/src_generic/components/AdminLayout.tsx index c2dece2..6e1daa1 100644 --- a/src_generic/components/AdminLayout.tsx +++ b/src_generic/components/AdminLayout.tsx @@ -18,13 +18,13 @@ import TableViewIcon from '@mui/icons-material/TableView'; import DashboardIcon from '@mui/icons-material/Dashboard'; import LogoutIcon from '@mui/icons-material/Logout'; import { ResourceConfig } from '../types/config'; +import { useLocation, useNavigate } from 'react-router-dom'; const drawerWidth = 240; interface AdminLayoutProps { children: React.ReactNode; onSelectResource: (resourceName: string | null) => void; - selectedResourceName: string | null; onLogout: () => void; username?: string; resources: ResourceConfig[]; @@ -33,11 +33,14 @@ interface AdminLayoutProps { export default function AdminLayout({ children, onSelectResource, - selectedResourceName, onLogout, username, resources, }: AdminLayoutProps) { + const location = useLocation(); + const navigate = useNavigate(); + const activeResourceName = location.pathname.split('/')[1] || null; + return ( @@ -67,8 +70,8 @@ export default function AdminLayout({ onSelectResource(null)} + selected={location.pathname === '/'} + onClick={() => navigate('/')} > @@ -82,7 +85,7 @@ export default function AdminLayout({ {resources.map((res) => ( onSelectResource(res.name)} > diff --git a/src_generic/components/EnhancedTable.tsx b/src_generic/components/EnhancedTable.tsx index 294c495..bc12886 100644 --- a/src_generic/components/EnhancedTable.tsx +++ b/src_generic/components/EnhancedTable.tsx @@ -15,7 +15,9 @@ import { } from '@mui/x-data-grid'; import EditIcon from '@mui/icons-material/Edit'; 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 { config: ResourceConfig; @@ -34,6 +36,7 @@ export default function EnhancedTable({ onCreate, onNavigateToResource, }: EnhancedTableProps) { + const navigate = useNavigate(); const columns: GridColDef[] = React.useMemo(() => { const cols: GridColDef[] = Object.entries(config.fields).map(([key, field]) => { @@ -45,6 +48,9 @@ export default function EnhancedTable({ renderCell: (params: GridRenderCellParams) => { 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 if (field.formatter) { return field.formatter(value); @@ -99,7 +105,23 @@ export default function EnhancedTable({ // 4. Default renderings if (field.type === 'boolean') return value ? 'Yes' : 'No'; if (field.type === 'datetime' || field.type === 'date') { - return new Date(value).toLocaleString(); + return (value) ? new Date(value).toLocaleString() : ''; + } + + if (isPk) { + return ( + { + e.stopPropagation(); + const rowId = params.row[config.primaryKey]; + navigate(`/${config.name}/${rowId}`); + }} + > + {value} + + ); } return value; @@ -112,12 +134,17 @@ export default function EnhancedTable({ field: 'actions', type: 'actions', headerName: 'Actions', - width: 100, + width: 120, getActions: (params) => [ + } + label="View" + onClick={() => navigate(`/${config.name}/${params.id}`)} + />, } label="Edit" - onClick={() => onEdit(params.row)} + onClick={() => navigate(`/${config.name}/edit/${params.id}`)} />, } @@ -128,7 +155,7 @@ export default function EnhancedTable({ }); return cols; - }, [config, onEdit, onDelete, onNavigateToResource]); + }, [config, onDelete, navigate, onNavigateToResource]); return ( @@ -144,14 +171,10 @@ export default function EnhancedTable({ getRowId={(row) => { const pk = config.primaryKey; if (row[pk] !== undefined && row[pk] !== null) return row[pk]; - // Fallback: search for common ID fields - const fallbackKeys = ['id', 'uuid', 'pk']; + const fallbackKeys = ['id', '_id', 'uuid', 'pk']; for (const key of fallbackKeys) { 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)}`; }} disableRowSelectionOnClick diff --git a/src_generic/components/GenericForm.tsx b/src_generic/components/GenericForm.tsx index f860010..131d07f 100644 --- a/src_generic/components/GenericForm.tsx +++ b/src_generic/components/GenericForm.tsx @@ -22,6 +22,8 @@ interface GenericFormProps { onSave: (data: any) => Promise; onCancel: () => void; loading?: boolean; + readOnly?: boolean; + onEditClick?: () => void; } import { ConfigContext } from '../App'; @@ -32,6 +34,8 @@ export default function GenericForm({ onSave, onCancel, loading: saving, + readOnly = false, + onEditClick, }: GenericFormProps) { initialData = initialData || {}; const [formData, setFormData] = React.useState(initialData); @@ -39,18 +43,25 @@ export default function GenericForm({ const appConfig = React.useContext(ConfigContext); const handleChange = (key: string, value: any) => { + if (readOnly) return; setFormData((prev: any) => ({ ...prev, [key]: value })); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); + if (readOnly) return; onSave(formData); }; + const getTitle = () => { + if (readOnly) return `View ${config.label}`; + return initialData[config.primaryKey] ? `Edit ${config.label}` : `New ${config.label}`; + }; + return ( - {initialData[config.primaryKey] ? `Edit ${config.label}` : `New ${config.label}`} + {getTitle()} @@ -61,7 +72,7 @@ export default function GenericForm({ field={field} value={formData[key]} onChange={(val: any) => handleChange(key, val)} - disabled={field.readOnly} + disabled={readOnly || field.readOnly} uploadFile={uploadFile} uploading={uploading} baseUrl={appConfig?.baseUrl || ""} @@ -70,11 +81,17 @@ export default function GenericForm({ - + {readOnly ? ( + + ) : ( + + )} ); diff --git a/src_generic/components/ResourceView.tsx b/src_generic/components/ResourceView.tsx index 1452bdf..a4dbc2c 100644 --- a/src_generic/components/ResourceView.tsx +++ b/src_generic/components/ResourceView.tsx @@ -4,6 +4,7 @@ import { ResourceConfig } from '../types/config'; import { useResource } from '../hooks/useResource'; import GenericForm from './GenericForm'; import EnhancedTable from './EnhancedTable'; +import { useParams, useLocation, useNavigate, Routes, Route } from 'react-router-dom'; interface ResourceViewProps { config: ResourceConfig; @@ -11,68 +12,75 @@ interface ResourceViewProps { } export default function ResourceView({ config, onNavigateToResource }: ResourceViewProps) { - const [view, setView] = React.useState<'list' | 'create' | 'edit'>('list'); - const [selectedItem, setSelectedItem] = React.useState(null); + 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 { 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 updateMutation = useUpdate(); const deleteMutation = useDelete(); const handleEdit = (item: any) => { - setSelectedItem(item); - setView('edit'); + navigate(`/${config.name}/edit/${item[config.primaryKey]}`); }; const handleCreate = () => { - setSelectedItem(null); - setView('create'); + navigate(`/${config.name}/create`); }; const handleSave = async (formData: any) => { try { - if (view === 'edit') { - const id = formData[config.primaryKey]; - await updateMutation.mutateAsync({ id, data: formData }); + if (isEdit) { + await updateMutation.mutateAsync({ id: id!, data: formData }); } else { await createMutation.mutateAsync(formData); } - setView('list'); + navigate(`/${config.name}`); } catch (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?')) { - await deleteMutation.mutateAsync(id); + await deleteMutation.mutateAsync(itemId); } }; - if (isLoading) return ; - if (error) return Error loading {config.pluralLabel}; + if (isList && listQuery.isLoading) return ; + if ((isEdit || isView) && itemQuery.isLoading) return ; return ( - {view === 'list' ? ( + {isList ? ( navigate(`/${res}/${id}`)} /> ) : ( setView('list')} + onCancel={() => navigate(`/${config.name}`)} loading={createMutation.isPending || updateMutation.isPending} + readOnly={isView} + onEditClick={() => navigate(`/${config.name}/edit/${id}`)} /> )} diff --git a/src_generic/components/fields/ImageUploadField.tsx b/src_generic/components/fields/ImageUploadField.tsx index 4ad18fd..9183f60 100644 --- a/src_generic/components/fields/ImageUploadField.tsx +++ b/src_generic/components/fields/ImageUploadField.tsx @@ -7,6 +7,7 @@ interface ImageUploadFieldProps { onUpload: (file: File) => void; size?: number; baseUrl: string; + disabled?: boolean; } export default function ImageUploadField({ @@ -16,6 +17,7 @@ export default function ImageUploadField({ onUpload, size = 64, baseUrl, + disabled = false, }: ImageUploadFieldProps) { const imgSrc = value @@ -33,23 +35,25 @@ export default function ImageUploadField({ sx={{ width: size, height: size, borderRadius: 2 }} /> - + {!disabled && ( + + )} ); diff --git a/src_generic/hooks/useResource.ts b/src_generic/hooks/useResource.ts index 223aac9..5c14c4d 100644 --- a/src_generic/hooks/useResource.ts +++ b/src_generic/hooks/useResource.ts @@ -17,7 +17,7 @@ export function useResource(config: ResourceConfig) { }); // --- READ ONE --- - const useOne = (id: string | null) => + const useRead = (id: string | null) => useQuery({ queryKey: [name, "detail", id], queryFn: async () => { @@ -69,7 +69,7 @@ export function useResource(config: ResourceConfig) { return { useList, - useOne, + useRead, useCreate, useUpdate, useDelete,