garage-erp/docs/dashboard/crud/enhancement-plan.md
2026-03-27 16:20:46 +03:00

8.7 KiB
Raw Permalink Blame History

Enhancement Plan

This document identifies current gaps and improvement opportunities in the CRUD flow. Items are grouped by priority and annotated with implementation effort.


Summary

The current CRUD pattern is clean, composable, and type-safe. Most issues are edge cases or developer-experience refinements rather than critical bugs. The highest-priority items are the ones marked High.


High Priority

1. useAuthApi — Missing Memoization

File: shared/useApi.ts

Problem: createApi() is called on every render. Every component that calls useAuthApi() creates new ApiClient instances per render cycle. This can cause unintended React Query cache misses or stale closures in edge cases.

Fix:

// shared/useApi.ts
import { useMemo } from "react"

export const useAuthApi = () => {
  const token = useAuthStore(s => s.token)
  return useMemo(
    () => createApi({ headers: token ? { Authorization: `Bearer ${token}` } : undefined }),
    [token],
  )
}

Effort: XS (2 lines) — no API changes required.


2. Edit Mode — No Server Re-fetch on Dialog Open

File: shared/hooks/use-resource-form.ts, modules/customers/customer-form.tsx

Problem: When the "Edit" button is clicked, useResourcePage passes selectedItem (the in-memory table row) as initialData. The form is pre-populated from this snapshot. However:

  • The snapshot may be stale if the item was modified elsewhere.
  • On page refresh with ?dialog=true&resourceId=5 in the URL, selectedItem is null (not hydrated) → the form opens empty.

Fix: Add an initialize function to each feature form that fetches the full resource by ID:

// In CustomerForm (use-resource-form options)
initialize: (id) => api.customers.show(id),    // requires CrudClient.show()
queryKey: [CUSTOMER_ROUTES.BY_ID, resourceId],

// In CrudClient (packages/api/src/infra/crud-client.ts)
async show(id: string) {
  return this.get(this.byIdRoute, { params: { id } } as never)
}

Effort: S — needs CrudClient.show() added and each form updated to pass initialize.


3. FormDialog — Single Dialog Limitation

File: shared/components/form-dialog.tsx

Problem: Dialog state is keyed to fixed URL params dialog and resourceId. If two independent FormDialog instances are on the same page (e.g., a main resource form and a nested "Add Customer" side panel), they share the same URL params and will conflict.

Fix: Accept a configurable paramKey prop:

export const createFormDialogParams = (key: string) => ({
  [`${key}_dialog`]: parseAsBoolean.withDefault(false),
  [`${key}_resourceId`]: parseAsString,
})

Effort: S — requires updating FormDialog, useFormDialog, and useResourcePage to accept a paramKey.


Medium Priority

4. No Global Search / Filter Support

File: shared/data-view/table-view/use-data-table-query.ts, shared/data-view/table-view/search-params.ts

Problem: dataTableSearchParams only supports page, per_page, sort_by, sort_order. There is no standard way to add resource-specific filters (e.g., search by name, filter by status).

Proposed: Add an optional filters object to useDataTableQuery that maps to additional query params:

useDataTableQuery({
  queryKey: ["customers"],
  client,
  filters: {
    search: parseAsString,
    status: parseAsStringEnum(["active", "inactive"] as const),
  },
})
// → adds ?search=&status= params and includes them in client.list()

Effort: M — requires a design decision and updates to use-data-table-query, data-table.tsx, and resource-page.


5. Grid View — Not Implemented

File: shared/data-view/grid-view/ (empty directory)

Problem: The grid-view folder was scaffolded but never implemented. The data-view layer is clearly designed to support multiple views (table/grid), but no toggle exists.

Proposed:

  • Implement a GridView component that accepts the same DataViewProps as DataTable.
  • Add a view toggle (Table | Grid) in the ResourcePage header or DashboardHeader.
  • Persist the selected view in a URL param (?view=grid).

Effort: ML depending on grid card design requirements.


6. useMutation Error Handling — Not Reusable

File: modules/customers/customer-form.tsx (and all other feature forms)

Problem: The pattern of mapping ApiError.validationErrors to form.setError is duplicated in every form's onError handler.

Fix: Extract a useFormMutation hook:

// shared/hooks/use-form-mutation.ts
export function useFormMutation<TValues extends FieldValues, TResponse = unknown>(
  form: UseFormReturn<TValues>,
  options: UseMutationOptions<TResponse, Error, TValues>,
) {
  return useMutation({
    ...options,
    onError: (err, vars, ctx) => {
      if (err instanceof ApiError && err.validationErrors) {
        Object.entries(err.validationErrors).forEach(([field, msgs]) => {
          form.setError(field as keyof TValues as any, { message: msgs[0] })
        })
      }
      options.onError?.(err, vars, ctx)
    },
  })
}

Effort: XS — purely additive. Existing forms can be migrated incrementally.


7. CUSTOMER_CREATED_EVENT — Unused Custom Event

File: modules/customers/customer-form.tsx

Problem: CustomerForm dispatches window.dispatchEvent(new CustomEvent("customer:created")) on success, but nothing in the codebase listens to this event. Cache invalidation is already handled via onSuccessinvalidateQuery(). The event dispatch is dead code.

Fix: Remove the event dispatch from CustomerForm (and the CUSTOMER_CREATED_EVENT export) unless there is a known future use case, such as notifying a sibling component outside the React tree.

Effort: XS.


8. Pagination Meta Split — Inconsistency

File: shared/data-view/table-view/use-data-table-query.ts

Problem: useDataTableQuery returns pagination with pageCount: 1, total: 0 as placeholders. The real values come from data.meta and are calculated inside ResourcePage.tsx. This means:

  • useResourcePage consumers who render the table directly (outside ResourcePage) need to duplicate the pageCount/total derivation.
  • The pagination object returned by the hook is misleading until data loads.

Fix: Either move the meta derivation inside useDataTableQuery (requiring it to accept a response shape hint), or document this as an intentional split and annotate it.

Effort: XSS.


Low Priority / Nice to Have

9. Row Selection for Bulk Actions

There is no row selection or bulk-delete support. TanStack Table supports rowSelection state natively. Adding a checkbox column and a "Delete selected" toolbar would benefit resource-heavy pages.

10. Error Boundary Around Table and Form

If a render error occurs inside DataTable or a feature form, it will bubble up and crash the whole page. Wrapping with <ErrorBoundary> (e.g., via react-error-boundary) would improve resilience.

11. ConfirmDialog — Not Enforced in Layout

Problem: <ConfirmDialog /> must be manually mounted in app/(authenticated)/layout.tsx. There is no lint rule or runtime warning if it is missing. If a developer forgets to add it to a new layout, confirm() calls will silently resolve to false (no dialog shown, deletion blocked).

Fix: Add a development-mode warning inside the confirm() function if the store's resolve is never set after a timeout.

12. Column Visibility / Hide

ColumnHeader has a "Hide" dropdown menu item (via column.toggleVisibility(false)), but there is no global "Show columns" control to restore hidden columns. Either remove the hide option or add a column visibility popup to DataTable.

13. dataTableSearchParamsCache — Imported but Unused in App Router

dataTableSearchParamsCache is exported but the pages use "use client" throughout. If server components are introduced for any list page, the cache needs wiring in the layout via nuqs's SearchParamsProvider.


Checklist

  • #1 — Memoize useAuthApi
  • #2 — Add CrudClient.show() and wire initialize in feature forms
  • #3 — Make FormDialog param key configurable
  • #4 — Design and implement filter/search param support in useDataTableQuery
  • #5 — Implement GridView and view toggle
  • #6 — Extract useFormMutation hook
  • #7 — Remove unused CUSTOMER_CREATED_EVENT
  • #8 — Move pagination meta derivation into useDataTableQuery
  • #9 — Row selection + bulk actions
  • #10 — Error boundaries
  • #11 — Dev-mode warning for missing ConfirmDialog
  • #12 — Column visibility restore control
  • #13 — Wire SearchParamsProvider if server components are adopted