Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[docs] Add CRUD to themed example #4785

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use client';
import * as React from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { CrudProvider, List, Create, Edit, Show } from '@toolpad/core/Crud';
import { employeesDataSource, Employee, employeesCache } from '../../../mocks/employees';
import CustomDataGrid from '../../../components/CustomDataGrid';

function matchPath(pattern: string, pathname: string): string | null {
const regex = new RegExp(`^${pattern.replace(/:[^/]+/g, '([^/]+)')}$`);
const match = pathname.match(regex);
return match ? match[1] : null;
}

export default function EmployeesCrudPage() {
const pathname = usePathname();
const router = useRouter();

const rootPath = '/employees';
const listPath = rootPath;
const showPath = `${rootPath}/:employeeId`;
const createPath = `${rootPath}/new`;
const editPath = `${rootPath}/:employeeId/edit`;

const showEmployeeId = matchPath(showPath, pathname);
const editEmployeeId = matchPath(editPath, pathname);

const handleRowClick = React.useCallback(
(employeeId: string | number) => {
console.log('Clicked on row with ID', employeeId);
router.push(`${rootPath}/${String(employeeId)}`);
},
[router],
);

const handleCreateClick = React.useCallback(() => {
router.push(createPath);
}, [createPath, router]);

const handleEditClick = React.useCallback(
(employeeId: string | number) => {
router.push(`${rootPath}/${String(employeeId)}/edit`);
},
[router],
);

const handleCreate = React.useCallback(() => {
router.push(listPath);
}, [listPath, router]);

const handleEdit = React.useCallback(() => {
router.push(listPath);
}, [listPath, router]);

const handleDelete = React.useCallback(() => {
router.push(listPath);
}, [listPath, router]);

return (
<CrudProvider<Employee> dataSource={employeesDataSource} dataSourceCache={employeesCache}>
Copy link
Member

@apedroferreira apedroferreira Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll expose the dataGrid slot to the Crud component so we can just use that component here.

Copy link
Member

@apedroferreira apedroferreira Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to just add a TODO comment here for this for now as i guess we will only be able to adjust here after the next release once we merge #4786

{pathname === listPath ? (
<List<Employee>
initialPageSize={25}
onRowClick={handleRowClick}
onCreateClick={handleCreateClick}
onEditClick={handleEditClick}
slots={{
dataGrid: CustomDataGrid,
}}
/>
) : null}
{pathname === createPath ? (
<Create<Employee>
initialValues={{ title: 'New Employee' }}
onSubmitSuccess={handleCreate}
resetOnSubmit={false}
/>
) : null}
{pathname !== createPath && showEmployeeId ? (
<Show<Employee> id={showEmployeeId} onEditClick={handleEditClick} onDelete={handleDelete} />
) : null}
{editEmployeeId ? <Edit<Employee> id={editEmployeeId} onSubmitSuccess={handleEdit} /> : null}
</CrudProvider>
);
}
21 changes: 20 additions & 1 deletion examples/core/auth-nextjs-themed/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
'use client';
import * as React from 'react';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
import { usePathname, useParams } from 'next/navigation';
import { PageContainer } from '@toolpad/core/PageContainer';
import Copyright from '../components/Copyright';
import SidebarFooterAccount, { ToolbarAccountOverride } from './SidebarFooterAccount';

export default function Layout(props: { children: React.ReactNode }) {
const pathname = usePathname();
const params = useParams();
const [employeeId] = params.segments ?? [];

const title = React.useMemo(() => {
if (pathname === '/employees/new') {
return 'New Employee';
}
if (employeeId && pathname.includes('/edit')) {
return `Employee ${employeeId} - Edit`;
}
if (employeeId) {
return `Employee ${employeeId}`;
}
return undefined;
}, [employeeId, pathname]);

return (
<DashboardLayout
slots={{
toolbarAccount: ToolbarAccountOverride,
sidebarFooter: SidebarFooterAccount,
}}
>
<PageContainer>
<PageContainer title={title}>
{props.children}
<Copyright sx={{ my: 4 }} />
</PageContainer>
Expand Down
20 changes: 16 additions & 4 deletions examples/core/auth-nextjs-themed/app/components/CustomDataGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import * as React from 'react';
import { DataGrid } from '@mui/x-data-grid';
import { columns, rows } from '../mocks/gridOrdersData';

export default function CustomizedDataGrid() {
export default function CustomizedDataGrid({
rows,
columns,
...rest
}: {
rows?: any;
columns?: any;
}) {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<DataGrid
checkboxSelection
rows={rows}
columns={columns}
{...rest}
checkboxSelection
getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd')}
initialState={{
pagination: { paginationModel: { pageSize: 20 } },
Expand All @@ -23,9 +30,14 @@ export default function CustomizedDataGrid() {
})}
pageSizeOptions={[10, 20, 50]}
disableColumnResize
density="compact"
slotProps={{
filterPanel: {
sx: {
'& .MuiDataGrid-filterForm': {
columnGap: 1.5,
marginTop: 2,
},
},
filterFormProps: {
logicOperatorInputProps: {
variant: 'outlined',
Expand Down
7 changes: 7 additions & 0 deletions examples/core/auth-nextjs-themed/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import { NextAppProvider } from '@toolpad/core/nextjs';
import PersonIcon from '@mui/icons-material/Person';
import DashboardIcon from '@mui/icons-material/Dashboard';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
Expand All @@ -22,6 +23,12 @@ const NAVIGATION: Navigation = [
title: 'Orders',
icon: <ShoppingCartIcon />,
},
{
segment: 'employees',
title: 'Employees',
icon: <PersonIcon />,
pattern: 'employees{/:employeeId}*',
},
];

const AUTHENTICATION = {
Expand Down
211 changes: 211 additions & 0 deletions examples/core/auth-nextjs-themed/app/mocks/employees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
'use client';
import { DataModel, DataSource, DataSourceCache } from '@toolpad/core/Crud';
import { z } from 'zod';

type EmployeeRole = 'Market' | 'Finance' | 'Development';

export interface Employee extends DataModel {
id: number;
name: string;
age: number;
joinDate: string;
role: EmployeeRole;
}

const INITIAL_EMPLOYEES_STORE: Employee[] = [
{
id: 1,
name: 'Edward Perry',
age: 25,
joinDate: new Date().toISOString(),
role: 'Finance',
},
{
id: 2,
name: 'Josephine Drake',
age: 36,
joinDate: new Date().toISOString(),
role: 'Market',
},
{
id: 3,
name: 'Cody Phillips',
age: 19,
joinDate: new Date().toISOString(),
role: 'Development',
},
];

const getEmployeesStore = (): Employee[] => {
const value = localStorage.getItem('employees-store');
return value ? JSON.parse(value) : INITIAL_EMPLOYEES_STORE;
};

const setEmployeesStore = (value: Employee[]) => {
return localStorage.setItem('employees-store', JSON.stringify(value));
};

export const employeesDataSource: DataSource<Employee> = {
fields: [
{ field: 'id', headerName: 'ID' },
{ field: 'name', headerName: 'Name', width: 140 },
{ field: 'age', headerName: 'Age', type: 'number' },
{
field: 'joinDate',
headerName: 'Join date',
type: 'date',
valueGetter: (value: string) => value && new Date(value),
width: 140,
},
{
field: 'role',
headerName: 'Department',
type: 'singleSelect',
valueOptions: ['Market', 'Finance', 'Development'],
width: 160,
},
],
getMany: async ({ paginationModel, filterModel, sortModel }) => {
// Simulate loading delay
await new Promise((resolve) => {
setTimeout(resolve, 750);
});

const employeesStore = getEmployeesStore();

let filteredEmployees = [...employeesStore];

// Apply filters (example only)
if (filterModel?.items?.length) {
filterModel.items.forEach(({ field, value, operator }) => {
if (!field || value == null) {
return;
}

filteredEmployees = filteredEmployees.filter((employee) => {
const employeeValue = employee[field];

switch (operator) {
case 'contains':
return String(employeeValue).toLowerCase().includes(String(value).toLowerCase());
case 'equals':
return employeeValue === value;
case 'startsWith':
return String(employeeValue).toLowerCase().startsWith(String(value).toLowerCase());
case 'endsWith':
return String(employeeValue).toLowerCase().endsWith(String(value).toLowerCase());
case '>':
return (employeeValue as number) > value;
case '<':
return (employeeValue as number) < value;
default:
return true;
}
});
});
}

// Apply sorting
if (sortModel?.length) {
filteredEmployees.sort((a, b) => {
for (const { field, sort } of sortModel) {
if ((a[field] as number) < (b[field] as number)) {
return sort === 'asc' ? -1 : 1;
}
if ((a[field] as number) > (b[field] as number)) {
return sort === 'asc' ? 1 : -1;
}
}
return 0;
});
}

// Apply pagination
const start = paginationModel.page * paginationModel.pageSize;
const end = start + paginationModel.pageSize;
const paginatedEmployees = filteredEmployees.slice(start, end);

return {
items: paginatedEmployees,
itemCount: filteredEmployees.length,
};
},
getOne: async (employeeId) => {
// Simulate loading delay
await new Promise((resolve) => {
setTimeout(resolve, 750);
});

const employeesStore = getEmployeesStore();

const employeeToShow = employeesStore.find((employee) => employee.id === Number(employeeId));

if (!employeeToShow) {
throw new Error('Employee not found');
}
return employeeToShow;
},
createOne: async (data) => {
// Simulate loading delay
await new Promise((resolve) => {
setTimeout(resolve, 750);
});

const employeesStore = getEmployeesStore();

const newEmployee = { id: employeesStore.length + 1, ...data } as Employee;

setEmployeesStore([...employeesStore, newEmployee]);

return newEmployee;
},
updateOne: async (employeeId, data) => {
// Simulate loading delay
await new Promise((resolve) => {
setTimeout(resolve, 750);
});

const employeesStore = getEmployeesStore();

let updatedEmployee: Employee | null = null;

setEmployeesStore(
employeesStore.map((employee) => {
if (employee.id === Number(employeeId)) {
updatedEmployee = { ...employee, ...data };
return updatedEmployee;
}
return employee;
}),
);

if (!updatedEmployee) {
throw new Error('Employee not found');
}
return updatedEmployee;
},
deleteOne: async (employeeId) => {
// Simulate loading delay
await new Promise((resolve) => {
setTimeout(resolve, 750);
});

const employeesStore = getEmployeesStore();

setEmployeesStore(employeesStore.filter((employee) => employee.id !== Number(employeeId)));
},
validate: z.object({
name: z.string({ required_error: 'Name is required' }).nonempty('Name is required'),
age: z.number({ required_error: 'Age is required' }).min(18, 'Age must be at least 18'),
joinDate: z
.string({ required_error: 'Join date is required' })
.nonempty('Join date is required'),
role: z.enum(['Market', 'Finance', 'Development'], {
errorMap: () => ({
message: 'Role must be "Market", "Finance" or "Development"',
}),
}),
})['~standard'].validate,
};

export const employeesCache = new DataSourceCache();
Loading