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 · 3 comments
Open

Add CRUD component #4146

apedroferreira opened this issue Sep 24, 2024 · 3 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
  • align datasource with X datasource
  • separate create/update/delete methods from datasource

Some Possible Additional Use Cases (not supported in first implementation)

  • 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 data source.
The CRUD component sets predefined routes for each of those 4 components, and automatically integrates with any Javascript web framework being used by the project.

<CRUD dataSource={ordersDataSource} rootPath="/orders" />

or for deeper customization, separate components can be placed in their own pages under a common CRUD provider with the data source:

<CRUD.CRUDProvider dataSource={ordersDataSource} />
  // ...
  <List 
    initialPageSize={initialPageSize}
    onRowClick={handleRowClick}
    onCreateClick={handleCreateClick}
    onEditClick={handleEditClick} 
  />
  <Show id={resourceId} onEditClick={handleEditClick} onDelete={handleDelete} />
  <Create initialValues={defaultValues} onSubmitSuccess={handleCreate} />
  <Edit id={resourceId} onSubmitSuccess={handleEdit} />
   // ...
</CRUD.CRUDProvider>

Data Source

Our concept of a data source has a fields property with the exact same shape as the MUI X DataGrid columns definition, but an additional longString type for multiline form fields. For example:

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

Besides fields, there are methods to interact with data in the data source object, as well as the option to provide a function for form validation. Here's the type definition for the initial data source:

export type DataModelId = string | number;

export interface DataModel {
  id: DataModelId;
  [key: string]: unknown;
}

export interface GetManyParams {
  paginationModel: GridPaginationModel;
  sortModel: GridSortModel;
  filterModel: GridFilterModel;
}

export type DataField = Omit<GridColDef, 'type'> & { type?: GridColType | 'longString' };

export interface DataSource<D extends DataModel> {
  fields: DataField[];
  getMany?: (params: GetManyParams) => Promise<{ items: D[]; itemCount: number }>;
  getOne?: (id: DataModelId) => Promise<D>;
  createOne?: (data: Omit<D, 'id'>) => Promise<D>;
  updateOne?: (data: D) => Promise<D>;
  deleteOne?: (id: DataModelId) => Promise<void>;
  /**
   * Function to validate form values. Returns object with error strings for each field.
   */
  validate: (
    formValues: Omit<D, 'id'>,
  ) => Partial<Record<keyof D, string>> | Promise<Partial<Record<keyof D, string>>>;
}

It should be easy to replace it with the MUI X data sources once they are complete and available, as they use a similar API.

List component

<List 
  dataSource={ordersDataSource}  
  initialPageSize={initialPageSize}
  onRowClick={handleRowClick}
  onCreateClick={handleCreateClick}
  onEditClick={handleEditClick}
/>

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

By default, the rightmost cell includes "Edit" (typically 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 } from '@mui/x-data-grid-pro';

function OrdersList() {
  return (
      <List 
        dataSource={ordersDataSource}
        slots={{ dataGrid: DataGridPro }} 
        slotProps={{ 
          dataGrid: { 
            getDetailPanelContent: ({ row }) => <div>Row ID: {row.id}</div>, 
            getDetailPanelHeight: ({ row }) => 'auto' } }} />
         }
      }}
  )
}

Possible inline/quick edit implementation:

const handleEditClick = () => {
  // opens a popover with the form. The form is a reusable component, as it's already being used in `Create` and `Edit`.
}

<List 
  dataSource={ordersDataSource}
  onEditClick={handleEditClick}
/>
<List dataSource={ordersDataSource} inlineEdit />

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

Including a createOne method in the data source renders a "Create New"/"Add new" button in the List view above the table.

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 id={orderId} dataSource={ordersDataSource} onEditClick={handleEditClick} onDelete={handleDelete} />

Create & Edit components

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

<Edit id={orderId} dataSource={ordersDataSource}} onSubmitSuccess={handleEdit} />
<Create dataSource={ordersDataSource}} initialValues={defaultValues} onSubmitSuccess={handleCreate} />

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

The forms are controlled forms using react state, and using no particular form library under the hood.
Through the validate function in the data source, these forms can be integrated with any library, such as yup or zod.

We could eventually provide adapters for the most popular schema libraries - for now, they should be present in examples and easy to copy.

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)

@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>

@apedroferreira
Copy link
Member Author

Updated RFC to match the first implementation in #4486
Quite a few changes were made along the way while exploring during the implementation.

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