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] Improve Toolpad Core docs #43796

Merged
merged 20 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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
2 changes: 1 addition & 1 deletion docs/data/material/components/app-bar/app-bar.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ You can override this behavior by setting the `enableColorOnDark` prop to `true`

{{"demo": "EnableColorOnDarkAppBar.js", "bg": true}}

## Experimental APIs
## Toolpad
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved

### DashboardLayout
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved

oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
4 changes: 2 additions & 2 deletions docs/data/material/components/breadcrumbs/breadcrumbs.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ The accessibility of this component relies on:
- To prevent screen reader announcement of the visual separators between links, they are hidden with `aria-hidden`.
- A nav element labeled with `aria-label` identifies the structure as a breadcrumb trail and makes it a navigation landmark so that it is easy to locate.

## Experimental APIs
## Toolpad

### Page Container

The [PageContainer](https://mui.com/toolpad/core/react-page-container/) component in `@toolpad/core` is the ideal wrapper for the content of your dashboard. It makes the Material UI Container navigation aware and extends it with page title, breadcrumbs, actions, and more.
The [PageContainer](https://mui.com/toolpad/core/react-page-container/) component in `@toolpad/core` is the ideal wrapper for the content of your dashboard. It makes the Material UI Container navigation-aware and extends it with page title, breadcrumbs, actions, and more.

{{"demo": "./PageContainerBasic.js", "height": 400, "hideToolbar": true}}
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions docs/data/material/components/container/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ The max-width matches the min-width of the current breakpoint.
<Container fixed>
```

## Experimental APIs
## Toolpad

### Page Container

The [PageContainer](https://mui.com/toolpad/core/react-page-container/) component in `@toolpad/core` is the ideal wrapper for the content of your dashboard. It makes the Material UI Container navigation aware and extends it with page title, breadcrumbs, actions, and more.
The [PageContainer](https://mui.com/toolpad/core/react-page-container/) component in `@toolpad/core` is the ideal wrapper for the content of your dashboard. It makes the Material UI Container navigation-aware and extends it with page title, breadcrumbs, actions, and more.

{{"demo": "../breadcrumbs/PageContainerBasic.js", "height": 400, "hideToolbar": true}}
Copy link
Member

@oliviertassinari oliviertassinari Sep 29, 2024

Choose a reason for hiding this comment

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

The dark/light mode is broken on all those demos.

As far as I understand things, theme={demoTheme} shouldn't be provided, not be the AppProvider responsibility to have this. Imagine I already have an application configured with a theme, I want some of my pages to have a Toolpad container, this shouldn't break. I mean, https://marmelab.com/react-admin/Theming.html feels wrong, no?

At minimum, MUI System should support theme nesting where you can inherit the light/dark mode and use different values for the other values.

Copy link
Member

@oliviertassinari oliviertassinari Sep 29, 2024

Choose a reason for hiding this comment

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

const demoWindow = window !== undefined ? window() : undefined; feels confusing, I think it should copy

* Injected by the documentation to work in an iframe.

to be clear.

120 changes: 120 additions & 0 deletions docs/data/material/components/dialogs/ToolpadDialogs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { DialogsProvider, useDialogs } from '@toolpad/core/useDialogs';
import Button from '@mui/material/Button';
import LoadingButton from '@mui/lab/LoadingButton';
import Dialog from '@mui/material/Dialog';
import Alert from '@mui/material/Alert';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';

function MyCustomDialog({ open, onClose, payload }) {
return (
<Dialog fullWidth open={open} onClose={() => onClose()}>
<DialogTitle>Custom Error Handler</DialogTitle>
<DialogContent>
<Alert severity="error">
{`An error occurred while deleting item "${payload.id}":`}
<pre>{payload.error}</pre>
</Alert>
</DialogContent>
<DialogActions>
<Button onClick={() => onClose()}>Close me</Button>
</DialogActions>
</Dialog>
);
}

MyCustomDialog.propTypes = {
/**
* A function to call when the dialog should be closed. If the dialog has a return
* value, it should be passed as an argument to this function. You should use the promise
* that is returned to show a loading state while the dialog is performing async actions
* on close.
* @param result The result to return from the dialog.
* @returns A promise that resolves when the dialog can be fully closed.
*/
onClose: PropTypes.func.isRequired,
/**
* Whether the dialog is open.
*/
open: PropTypes.bool.isRequired,
/**
* The payload that was passed when the dialog was opened.
*/
payload: PropTypes.shape({
error: PropTypes.string,
id: PropTypes.string,
}).isRequired,
};

const mockApiDelete = async (id) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!id) {
reject(new Error('ID is required'));
} else if (parseInt(id, 10) % 2 === 0) {
console.log('id', parseInt(id, 10));
resolve(true);
} else if (parseInt(id, 10) % 2 === 1) {
reject(new Error('Can not delete odd numbered elements'));
} else if (Number.isNaN(parseInt(id, 10))) {
reject(new Error('ID must be a number'));
} else {
reject(new Error('Unknown error'));
}
}, 1000);
});
};

function DemoContent() {
const dialogs = useDialogs();
const [isDeleting, setIsDeleting] = React.useState(false);

const handleDelete = async () => {
const id = await dialogs.prompt('Enter the ID to delete', {
okText: 'Delete',
cancelText: 'Cancel',
});

if (id) {
const deleteConfirmed = await dialogs.confirm(
`Are you sure you want to delete "${id}"?`,
);
if (deleteConfirmed) {
try {
setIsDeleting(true);
await mockApiDelete(id);
dialogs.alert('Deleted!');
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
await dialogs.open(MyCustomDialog, { id, error: message });
} finally {
setIsDeleting(false);
}
}
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', gap: 16 }}>
<LoadingButton
variant="contained"
loading={isDeleting}
onClick={handleDelete}
>
Delete
</LoadingButton>
</div>
</div>
);
}

export default function ToolpadDialogs() {
return (
<DialogsProvider>
<DemoContent />
</DialogsProvider>
);
}
101 changes: 101 additions & 0 deletions docs/data/material/components/dialogs/ToolpadDialogs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as React from 'react';
import { DialogsProvider, useDialogs, DialogProps } from '@toolpad/core/useDialogs';
import Button from '@mui/material/Button';
import LoadingButton from '@mui/lab/LoadingButton';
import Dialog from '@mui/material/Dialog';
import Alert from '@mui/material/Alert';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';

interface DeleteError {
id: string | null;
error: string | null;
}

function MyCustomDialog({ open, onClose, payload }: DialogProps<DeleteError>) {
return (
<Dialog fullWidth open={open} onClose={() => onClose()}>
<DialogTitle>Custom Error Handler</DialogTitle>
<DialogContent>
<Alert severity="error">
{`An error occurred while deleting item "${payload.id}":`}
<pre>{payload.error}</pre>
</Alert>
</DialogContent>
<DialogActions>
<Button onClick={() => onClose()}>Close me</Button>
</DialogActions>
</Dialog>
);
}

const mockApiDelete = async (id: string | null) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!id) {
reject(new Error('ID is required'));
} else if (parseInt(id, 10) % 2 === 0) {
console.log('id', parseInt(id, 10));
resolve(true);
} else if (parseInt(id, 10) % 2 === 1) {
reject(new Error('Can not delete odd numbered elements'));
} else if (Number.isNaN(parseInt(id, 10))) {
reject(new Error('ID must be a number'));
} else {
reject(new Error('Unknown error'));
}
}, 1000);
});
};

function DemoContent() {
const dialogs = useDialogs();
const [isDeleting, setIsDeleting] = React.useState(false);

const handleDelete = async () => {
const id = await dialogs.prompt('Enter the ID to delete', {
okText: 'Delete',
cancelText: 'Cancel',
});

if (id) {
const deleteConfirmed = await dialogs.confirm(
`Are you sure you want to delete "${id}"?`,
);
if (deleteConfirmed) {
try {
setIsDeleting(true);
await mockApiDelete(id);
dialogs.alert('Deleted!');
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
await dialogs.open(MyCustomDialog, { id, error: message });
} finally {
setIsDeleting(false);
}
}
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', gap: 16 }}>
<LoadingButton
variant="contained"
loading={isDeleting}
onClick={handleDelete}
>
Delete
</LoadingButton>
</div>
</div>
);
}

export default function ToolpadDialogs() {
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved
return (
<DialogsProvider>
<DemoContent />
</DialogsProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<DialogsProvider>
<DemoContent />
</DialogsProvider>
41 changes: 36 additions & 5 deletions docs/data/material/components/dialogs/dialogs.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,39 @@ The package [`material-ui-confirm`](https://github.com/jonatanklosko/material-ui

Follow the [Modal accessibility section](/material-ui/react-modal/#accessibility).

## Experimental APIs

### Imperative API

You can create and manipulate dialogs imperatively with the [`useDialog`](https://mui.com/toolpad/core/react-use-dialogs/) API in `@toolpad/core`. This API provides state management for opening and closing dialogs and for passing data to the dialog and back. It allows for stacking multiple dialogs. It also provides themed alternatives for `window.alert`, `window.confirm` and `window.prompt`.
## Toolpad

### `useDialogs`
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved

You can create and manipulate dialogs imperatively with the [`useDialogs`](https://mui.com/toolpad/core/react-use-dialogs/) API in `@toolpad/core`. This API provides state management for opening and closing dialogs and for passing data to the dialog and back. It allows for stacking multiple dialogs. It also provides themed alternatives for `window.alert`, `window.confirm` and `window.prompt`.
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved

The following example demonstrates how the `useDialogs` API can create a dialog that prompts the user to enter an ID and then confirms the deletion of an item with that ID. It waits for the async delete operation to complete before either displaying a success alert or a custom error dialog with an error payload:
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved

{{"demo": "ToolpadDialogs.js", "hideToolbar": "true"}}
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved

```tsx
const handleDelete = async () => {
const id = await dialogs.prompt('Enter the ID to delete', {
okText: 'Delete',
cancelText: 'Cancel',
});

if (id) {
const deleteConfirmed = await dialogs.confirm(
`Are you sure you want to delete "${id}"?`,
);
if (deleteConfirmed) {
try {
setIsDeleting(true);
await mockApiDelete(id);
dialogs.alert('Deleted!');
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
await dialogs.open(MyCustomDialog, { id, error: message });
} finally {
setIsDeleting(false);
}
}
}
};
```
2 changes: 1 addition & 1 deletion docs/data/material/components/drawers/drawers.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Apps focused on productivity that require balance across the screen.

{{"demo": "ClippedDrawer.js", "iframe": true}}

## Experimental APIs
## Toolpad

### DashboardLayout
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved

oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
53 changes: 53 additions & 0 deletions docs/data/material/components/snackbars/ToolpadNotifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as React from 'react';
import {
useNotifications,
NotificationsProvider,
} from '@toolpad/core/useNotifications';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';

function NotifyRadioButton() {
const notifications = useNotifications();
const [online, setOnline] = React.useState(true);
const prevOnline = React.useRef(online);
React.useEffect(() => {
if (prevOnline.current === online) {
return () => {};
}
prevOnline.current = online;

// preview-start
const key = online
? notifications.show('You are now online', {
severity: 'success',
autoHideDuration: 3000,
})
: notifications.show('You are now offline', {
severity: 'error',
});

return () => {
notifications.close(key);
};
// preview-end
}, [notifications, online]);

return (
<div>
<FormControlLabel
control={
<Switch checked={online} onChange={() => setOnline((prev) => !prev)} />
}
label="Online"
/>
</div>
);
}

export default function ToolpadNotifications() {
return (
<NotificationsProvider>
<NotifyRadioButton />
</NotificationsProvider>
);
}
Loading