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

Add CRUD component #4146

Open
apedroferreira opened this issue Sep 24, 2024 · 2 comments
Open

Add CRUD component #4146

apedroferreira opened this issue Sep 24, 2024 · 2 comments
Assignees
Labels
linked in docs The issue is linked in the docs, so completely skew the upvotes new feature New feature or request scope: toolpad-core Abbreviated to "core" waiting for 👍 Waiting for upvotes

Comments

@apedroferreira
Copy link
Member

apedroferreira commented Sep 24, 2024

High level proposal

  • start from CRUD prototype/RFC ([RFC] Dashboard/CRUD framework #3311)
  • separate column definition from datasource => prop on CRUD component
  • align datasource with X datasource
  • separate create/update/delete methods from datasource => props on the CRUD component

Some Additional Use Cases

  • Extending default form pages, for example to be able to perform partial update actions individually, separately from the main "update" form (toggles, for example) (e.g.: ban a user)
  • Different page layouts for more readable forms (e.g.: two-column layout, group fields, tabs)
  • Quick-edit from list items, in a form in its own modal/dialog over the table

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

  • https://refine.dev/docs/#use-cases
    Shows a full featured CRUD component with separate views for edit, create, show and a dashboard layout with sign
    in/sign out.

  • https://refine.dev/docs/guides-concepts/forms/#edit

  • Exposes a useForm hook which is a wrapper over react-hook-form and integrates with Refine's dataProvider

  • Exposes props such as saveButtonProps and the individual field props when you use useForm with a resource parameter, you define all the form fields individually yourself

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

React-Admin

  • https://marmelab.com/react-admin/Edit.html -> You specify form UIs yourself with their custom components, such as TextField from react-admin etc.

    <Edit>
            <SimpleForm>
                <TextInput disabled label="Id" source="id" />
                <TextInput source="title" validate={required()} />         
            </SimpleForm>
        </Edit>
  • Out of the box UI for CRUD forms does not use visual space efficiently - easy area to improve on

  • Has a paid EditInDialog / CreateInDialog component that allows quick edits

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.

@apedroferreira apedroferreira self-assigned this Sep 24, 2024
@apedroferreira apedroferreira converted this from a draft issue Sep 24, 2024
@github-actions github-actions bot added the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Sep 24, 2024
@bharatkashyap bharatkashyap added scope: toolpad-core Abbreviated to "core" and removed status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Sep 25, 2024
@aress31
Copy link

aress31 commented Oct 30, 2024

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! 🔥

@prakhargupta1 prakhargupta1 added linked in docs The issue is linked in the docs, so completely skew the upvotes new feature New feature or request labels Nov 4, 2024
@oliviertassinari oliviertassinari added the waiting for 👍 Waiting for upvotes label Nov 4, 2024
@prakhargupta1 prakhargupta1 added this to the Toolpad Core MVP milestone Dec 13, 2024
@Janpot
Copy link
Member

Janpot commented Jan 14, 2025

Additionally we could also consider exporting a composable API to give users even more control over e.g. custom routing, richer details page, nesting resources,...

// Define data sources
const myUsers = createRestData('https://api.example.com/users', {
  fields: {
    id: {},
    name: {},
    email: {}
  }
})

const myInvoices = createRestData('https://api.example.com/invoices', {
  fields: {
    id: {},
    user_id: {},
    date: {}
  }
})

// Extended details view for resource
function Details() {
  const {id} = useParams()
  
  // derived data for a specific user
  const userInvoices = useFilteredData(myInvoices, {
    filter: {
      user_id: id
    }
  })

  return (
    <div>
      <Crud.Details id={id} />
      <Crud.List data={userInvoices} />
    </div>
  )
}

function Edit() {
  const {id} = useParams()
  return <Crud.Edit id={id} />
}

// Composable API
<Crud.Provider data={myUsers} create='./new' edit={id => `./details/${id}`}>
  <Routes>
    <Route path="/" element={<Crud.List />} />
    <Route path="/new" element={<Crud.Create />} />
    <Route path="/details:id" element={<Details />} />
    <Route path="/edit/:id" element={<Edit />} />
  </Routes>
</Crud.Provider>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
linked in docs The issue is linked in the docs, so completely skew the upvotes new feature New feature or request scope: toolpad-core Abbreviated to "core" waiting for 👍 Waiting for upvotes
Projects
Status: In progress
Development

No branches or pull requests

6 participants