mui / toolpad

Toolpad: Full stack components and low-code builder for dashboards and internal apps.
https://mui.com/toolpad/
MIT License
1.3k stars 292 forks source link

Add CRUD component #4146

Open apedroferreira opened 2 months ago

apedroferreira commented 2 months ago

High level proposal

Some Additional Use Cases

Possible implementation

Separate List/Show/Create and Edit components to generate a page from same model definition. The Crud component sets predefined routes for each of those 4 components, but still allows for overriding those routes with slots.

<Crud 
  fields={fieldsDefinition} 
  methods={{ createOne: () => {}, updateOne: () => {} }}
  // optional overrides
  slots={{ createOne: Create, update: Update, list: List }}}
/>

What about overriding the default paths if slots are the way to override these primary components?

To be figured out in v2, but after internal discussion we decided to start with a slots prop. Possibly, the paths could be set/overriden with a new separate prop.

Fields Definition

We will start by extending the column definition from the MUI X DataGrid as much as possible, such as:

{
    field: 'firstName',
    headerName: 'First name',
  },
  {
    field: 'lastName',
    headerName: 'Last name',
  },
  {
    field: 'age',
    headerName: 'Age',
    type: 'number',
  },

List component

<List 
  fields={fieldsDefinition} 
  methods={{ getMany: () => {}, updateOne: () => {}, deleteOne: () => {} }}
/>

Generates a page with a DataGrid showing the fetched items and column types derived from the fields definition.

By default, the rightmost cell includes an options button that opens a popover menu with "Edit" (redirects to the /edit page) and "Delete" (with a confirmation dialog) options.

If someone wants to customize the behavior of the underlying data grid (e.g.: use the Pro data grid and its features), they can use the dataGrid slot and slot props:

import { DataGridPro, GridColDef } from '@mui/x-data-grid-pro';

function OrdersList() {
  return (
      <List 
        fields={fieldsDefinition} 
        methods={{ getMany: () => {}, updateOne: () => {}, deleteOne: () => {} }} 
        slots={{ dataGrid: DataGridPro }} 
        slotProps={{ 
          dataGrid: { 
            getDetailPanelContent: ({ row }) => <div>Row ID: {row.id}</div>, 
            getDetailPanelHeight: ({ row }) => 'auto' } }} />
         }
      }}
  )
}

Possible inline/quick edit implementation:

function QuickEdit(props: ListQuickEditProps) { 
 const { form, fields } = useForm()

 return (
   <>
     <TextField {...fields.firstName} />
     <TextField {...fields.lastName} />
   </>
  )
}

<List 
  fields={fieldsDefinition} 
  methods={{ getMany: () => {}, updateOne: () => {}, deleteOne: () => {} }} 
  slots={{ quickEdit: <QuickEdit /> }} 
  slotProps={{ quickEdit: { container: "drawer" }}}
/>
<List 
  fields={fieldsDefinition} 
  methods={{ getMany: () => {}, updateOne: () => {}, deleteOne: () => {} }} 
  inlineEdit
/>

(uses the default inline editing features of the DataGrid and shows an inline edit button along with the options menu)

Adding a createOne method will render a "Create New"/"Add new" button in the List view above the table, which is overridable through a slot:

<List   
 methods={{ createOne: () => {} }}
 slot={{ createButton: <CreateButton /> }}
/>

Show component

This component corresponds to the details of an individual list item, usually accessible when you click on an individual row in from the List

<Show fields={fieldsDefinition} methods={{ getOne: () => {} }} />

Customization:

function CustomShow() {
 const record = useRecord();

  return (
    <Paper>
       <Typography>Name: {record.title}</Typography>
       // custom content
     </Paper>
   )
}

<Show fields={fieldDefinition} methods={{ getOne: () => {} }} slots={{ show: CustomShow }} />

Create & Edit components

These will be separate components with similar functionality but slightly different default content (submit button text, for example).

<Edit fields={fieldDefinition} methods={{ updateOne: () => {} }} form={formImpl} />
<Create fields={fieldDefinition} methods={{ createOne: () => {} }} form={formImpl} />

(Auto-renders a form with the fields based on the fields definition)

The form abstractions should be agnostic so that any form library can be used with the generated forms. This means that a form implementation has to be passed in to fully configure these components. The full definition of this prop should be finalized while testing actual integrations, but an initial idea could be:

interface Form<V extends Record<string, unknown>> {
  value: V
  onChange: (newValue: unknown) => V
  onReset: () => V
}

We can provide out-of-the-box integrations with libraries such as react-hook-form.

Customization:

function CustomEdit() {
 const { form, fieldProps, defaultFormContent } = useForm()

 return (
   <>
     <p>Hello, {form.value.firstName}! </p>
     {defaultFormContent} // Render all the fields, or 
     <TextField {...fieldProps.firstName} /> // Manually render each field
     <Typography>Customized Stuff</Typography>
   </>
 )
}

const Edit => () => (
 <Edit fields={fieldDefinition} methods={{ update: () => {} }} slots={{ edit: CustomEdit }} />
)

This offers complete customizability, allows for using MUI components directly, and we can create blocks/components (paid or free) for different preset form content

Benchmark:

Refine

   const {
        saveButtonProps,
        refineCore: { query: productQuery },
        register,
        control,
    } = useForm<IProduct>({
        refineCoreProps: {
            resource: "products",
            action: "edit",
        },
    });

React-Admin

Tremor

Minimals CRUD (Visual benchmark)

To Clarify

Data providers for server-side data seem to only be available in the data grid pro plan for now. This means that probably for now we will not use that feature in our underlying implementation of CRUD. In the future perhaps we could do it in a sort of "pro" version of the CRUD? In any case, all server-side methods implemented in the CRUD will stick to the MUI X data provider implementation as closely as possible, as long as it makes sense to.

aress31 commented 3 weeks ago

Adding examples on the doc on how to link/use this newly planned CRUD component together with DataGrid, React Router and Firebase RTDB would be fantastic! 🔥