Show Loading States for Mutations — Implementation Plan¶
Decisions¶
| Decision | Choice | Why |
|---|---|---|
| Pending source | The owning React Router useNavigation or useFetcher state |
Each form or mutation can show feedback for its own request without unrelated dataset loads causing false busy states. |
| Button feedback | Spinner icon plus a short pending label | The user needs both an immediate visual signal and an unambiguous description of the action in progress. |
| Control locking | Disable the active form's submit, cancel, navigation, and conflicting action controls while pending | Prevents duplicate submissions and abandoning or contradicting an in-flight mutation. |
| Error behavior | Restore idle controls and preserve existing route/fetcher errors | Loading feedback should not replace the current validation and server-error contract. |
| Shared UI | Add the smallest shared pending-button/form pattern needed by repeated controls; keep mutation ownership in each existing route or feature | Consistent presentation is useful, but a global mutation store or server refactor would add unnecessary coupling. |
| Scope boundary | Include visible mutations; exclude dataset/detail loads, a global progress bar, server actions, authorization, and new error handling | The refined task is about making existing background mutations understandable. |
| Persistence/schema | No new server persistence or Directus field | Loading state is client presentation around existing actions; no directus/schema/*.json snapshot exists or is required. |
Prerequisites¶
- Keep the route table in
app/routes.tsunchanged. Existing route modules remain the owners of their forms and actions. - Preserve the server boundary in
app/lib/*.server.ts; this task does not change persistence, action intents, authorization, or response shapes. - Use the existing
Button,Field,FieldError, and lucide icon conventions. The pending icon must be decorative while the button exposes an accessible busy state and label. - Treat
navigation.formActionor an individual fetcher'sstateas the source of truth. Do not usenavigation.statealone where another form could be submitting. - Keep non-mutating
datasetFetcheranddatasetDetailFetcherout of mutation pending calculations. - Preserve current successful redirects, editor cleanup effects, and existing error rendering.
- No
directus/schema/*.jsonfiles are present; no schema change is part of this UI-only plan.
Step 1 — Establish the pending-control presentation pattern¶
Goal: Give all mutation surfaces one consistent, accessible way to render a pending button without changing their action ownership.
Scope
- In:
app/components/ui/button.tsxor a narrowly scoped adjacent app component, define the smallest reusable contract for pending label, spinner icon,disabled, andaria-busybehavior if repeated usage warrants it. - In: existing button/icon conventions, keep button dimensions stable when idle text changes and prevent the spinner from changing layout.
- In: document the distinction between a form's own pending state and unrelated fetcher states through naming and call-site usage.
- Out: A global provider, global progress bar, server changes, automatic interception of every form, and changes to the generated shadcn API beyond what the call sites need.
Acceptance criteria
- The pattern exposes an explicit pending state and idle/pending labels.
- Pending buttons remain keyboard-accessible, expose busy status, and cannot be activated twice.
- The spinner is not announced redundantly when the button label already communicates progress.
- The control does not shift size or wrap unexpectedly at the existing compact button sizes.
-
pnpm verify typecheckpasses.
Step 2 — Add pending states to authentication and route-level forms¶
Goal: Make login, logout, project creation, map creation, and project editing visibly busy and mutually exclusive while their existing submissions run.
Scope
- In:
app/routes/login.tsx, retain the existing route-specific pending detection and use the shared presentation; disable the login form controls while signing in and restore the current error behavior after a rejected action. - In:
app/routes/home.tsx, detect the/logoutsubmission specifically, show pending feedback on Sign out, and prevent a second logout or conflicting home-page action while it is pending. - In:
app/routes/create.tsxandapp/routes/add-map.tsx, derive pending state from the relevant navigation submission, show feedback for upload/create actions, and disable submit/cancel controls for the multipart request duration. - In:
app/routes/project.tsx, scope pending feedback to the project edit form and preserve the existing project action error display and redirect/update behavior. - In: use stable button labels and existing route semantics; do not add a second upload progress model or change the server action.
- Out: Map-object fetchers, map settings, and non-mutating route loads, which are handled in later steps.
Acceptance criteria
- Login shows a spinner and pending label only while the login submission is active.
- Logout shows a spinner and pending label only for the
/logoutsubmission. - Project creation, map creation, and project editing lock their active form actions during upload/save.
- Cancel/navigation controls cannot abandon or compete with the active submission.
- Successful redirects and existing validation/server errors remain unchanged.
-
pnpm verify typecheckpasses.
Step 3 — Add pending states to map settings and editor forms¶
Goal: Make the map editor's explicit add/edit/save/delete forms communicate their individual fetcher state and prevent conflicting actions.
Scope
- In:
app/routes/project-map.tsx, use each mutation fetcher's own state for settings, POI, line, area, and image-overlay actions; keep dataset and dataset-detail fetchers excluded. - In: the map settings form in
app/routes/project-map.tsx, add spinner/pending label feedback and disable the settings form actions whilesettingsFetcheris submitting. - In:
app/features/map-editor/edit-detail-panel.tsx, pass pending state through the existing form fetchers and lock each active add/update/delete or image-overlay save form without affecting unrelated editor panels. - In:
app/features/map-objects/map-object-form.tsx, add pending feedback and action locking for POI, line, and area add/update forms. - In:
app/features/map-objects/map-object-type-form.tsxandapp/features/map-objects/map-object-type-manager.tsx, cover type add/update actions and prevent closing or starting a conflicting type action while the relevant fetcher is pending. - In: preserve existing success effects that close forms or clear selection, and preserve current
FieldError/fetcher error rendering. - Out: Direct map gesture submissions and shared destructive confirmation, handled in Step 4.
Acceptance criteria
- Settings, object, overlay, and object-type forms show only their own pending state.
- Each pending submit control has a spinner and clear pending label.
- Cancel, close, and conflicting add/edit controls are disabled for the active form's request.
- A pending fetcher does not disable unrelated forms that can safely operate independently.
- Successful cleanup effects and failed-request error messages remain intact.
-
pnpm verify typecheckpasses.
Step 4 — Cover destructive confirmations and direct map mutations¶
Goal: Make delete, drag, and geometry-point mutations understandable even when they are submitted without a conventional form submit button.
Scope
- In:
app/components/confirm-delete-button.tsx, show pending feedback on the confirmation action afterfetcher.submit, disable both dialog actions while the delete fetcher is active, and restore them when it settles. - In:
app/features/map-objects/map-object-type-manager.tsxand map editor callers, ensure the shared delete control receives the correct independent fetcher state and cannot trigger duplicate deletes. - In:
app/routes/project-map.tsx, define pending behavior formoveFetcher,updateLinePointsFetcher, andupdateAreaPointsFetcher, which are submitted by map interactions rather than visible submit buttons. - In: the relevant map editor/map-object components, prevent conflicting editing or repeated gesture submissions while those fetchers are active and provide a compact, accessible busy indication tied to the active interaction.
- In: preserve normal map navigation and independent editing behavior once the mutation settles; do not make all map interaction unavailable for unrelated fetchers.
- Out: Dataset loading indicators, global map loading UI, optimistic persistence, and server-side concurrency changes.
Acceptance criteria
- Delete confirmation cannot submit twice and visibly indicates that deletion is in progress.
- A failed delete restores dialog actions and preserves the existing fetcher error path.
- POI movement and line/area point updates have a visible busy state tied to their own fetcher.
- Repeated gesture submissions are prevented while the same mutation is in flight.
- Unrelated map controls and independent fetchers are not blocked unnecessarily.
-
pnpm verify typecheckpasses.
Step 5 — Verify all mutation states and responsive behavior¶
Goal: Confirm every scoped mutation communicates progress, recovers cleanly, and does not regress existing map workflows.
Acceptance criteria
-
pnpm verify typecheckpasses. -
pnpm verify formatcompletes successfully. -
pnpm buildpasses. - Manual check: submit login with valid and invalid credentials; confirm pending feedback, redirect behavior, and existing error rendering.
- Manual check: submit logout, create project, add map, and edit project; confirm controls lock only for the relevant request and multipart uploads show pending feedback.
- Manual check: save map settings and each map-object/object-type form; confirm spinner/label, disabled conflicting actions, success cleanup, and error recovery.
- Manual check: open a delete confirmation, submit it, and confirm both dialog actions behave correctly during success and failure.
- Manual check: move a POI and edit line/area points; confirm direct mutations show busy feedback and do not permit duplicate submissions.
- Manual check: while a dataset or detail fetch is loading, confirm mutation buttons do not appear busy unless their own mutation is active.
- Manual check: check compact controls and forms at mobile and desktop widths for label wrapping, layout shifts, overlap, and readable focus states.
- Confirm no route action, persistence module, Directus schema, or authorization contract was changed unnecessarily.
Risks / open questions¶
- There is no test suite, so coverage depends on
pnpm verify typecheck,pnpm verify format,pnpm build, and the focused manual flows above. - Several map mutations are initiated by drag or point-edit gestures rather than buttons; the implementation must choose a small local busy indicator and interaction lock without turning the map into a globally disabled surface.
- Multiple fetchers can be active in the map route at once. Pending state must remain keyed to the owning fetcher, especially where the same editor panel can switch between add, update, and delete modes.
- The shared confirmation dialog may be reused by mutation types beyond object-type deletion. Its pending contract must remain generic and preserve existing trigger behavior.