Skip to content

Specification: Phase 3.3 — Component Updates

Update the 5 image-related components in ux-prototype for FD-01 compliance and lifecycle framework adoption. Source: ux-prototype-architectural-review.md (moderate-change components, excluding ItemCardEditor per FD-06).

Follow the ui-component skill for component structure (Static/Init/Runtime config separation, Storybook integration). Follow the unit-tests-frontend skill for test conventions.

  • Phase 3.2 complete: lifecycle types and useDraft<T> hook available
  • infrastructure#439 deployed: CloudFront CORS Response Headers Policy for https://*.arda.cards with Access-Control-Allow-Credentials: true. Required by T-1 change #7 (crossOrigin="use-credentials" on canvas image editing). The component code can be implemented and tested in Storybook without this (Storybook uses local blobs), but production edit-existing-image flow will fail without the CORS policy.
  • All existing checks pass

T-1: ImageUploadDialog — Indeterminate progress + UploadError

Section titled “T-1: ImageUploadDialog — Indeterminate progress + UploadError”

Source: REQ-FE-040, REQ-FE-041, REQ-FE-042

Changes:

  1. Remove the simulated 50ms progress timer (lines ~186-209 in current code)
  2. Replace <Progress value={phase.progress}> with an indeterminate indicator (spinner or pulsing bar with no percentage)
  3. Add UploadError phase to the state machine:
    type DialogPhase =
    | ... existing phases ...
    | { name: 'UploadError'; error: string; imageData: File | Blob | string };
  4. Add UPLOAD_ERROR action and transitions:
    • UploadingUploadError (on onUpload rejection)
    • UploadErrorUploading (retry)
    • UploadErrorEmptyImage (discard and start over)
  5. Render error message and retry/discard buttons in UploadError phase
  6. Adopt EditLifecycleCallbacks<ImageUploadResult> — the existing onConfirm/onCancel already match; add type import for documentation
  7. When the dialog opens in EditExisting state (editing a CDN-hosted image), the ImagePreviewEditor must set crossOrigin="use-credentials" on the <img> element that loads the CDN image into the crop canvas. Without this, the canvas is tainted by cross-origin content and toBlob() / getCroppedImage() throws a SecurityError.
    • react-easy-crop supports this via the mediaProps prop: mediaProps={{ crossOrigin: 'use-credentials' }}
    • This attribute must only be set when the image source is a CDN URL (matches *.assets.arda.cards). For local blobs and object URLs, crossOrigin should be omitted to avoid unnecessary CORS preflights.
    • Infrastructure prerequisite: CloudFront must serve CORS response headers (Access-Control-Allow-Origin, Access-Control-Allow-Credentials: true) via a Response Headers Policy. See infrastructure#439 — this ticket must be deployed before the edit-existing flow works in production.

Tests:

  • Uploading phase shows indeterminate indicator (no progress percentage)
  • UploadError phase renders error message
  • Retry from UploadError transitions back to Uploading
  • Discard from UploadError transitions to EmptyImage
  • Default onUpload handler still works in Storybook
  • EditExisting with CDN URL sets crossOrigin="use-credentials" on the crop image element
  • EditExisting with local blob URL does not set crossOrigin

T-2: ImageCellEditor — Factory with typed provider hooks

Section titled “T-2: ImageCellEditor — Factory with typed provider hooks”

Source: REQ-FE-043

Changes:

  1. Extend createImageCellEditor factory config to accept typed hooks as required fields:
    interface ImageCellEditorConfig {
    config: ImageFieldConfig;
    useImageUpload: () => { mutateAsync: (file: Blob) => Promise<string>; isPending: boolean };
    useCheckReachability: () => { mutateAsync: (url: string) => Promise<boolean> };
    }
  2. Pass hooks through to ImageUploadDialog as onUpload/onCheckReachability
  3. The ImageCellEditorConfig interface makes useImageUpload and useCheckReachability required (not optional). In Storybook, story-level config provides stub implementations via createImageCellEditor({ ..., useImageUpload: useStubImageUpload, useCheckReachability: useStubCheckReachability }). This shifts the missing-hook error from runtime to compile time — production column definitions that forget to wire hooks fail tsc, not silently at runtime.

Tests:

  • Factory creates editor component that renders ImageUploadDialog
  • Hooks are forwarded to dialog props
  • TypeScript compilation fails when hooks are omitted from config

T-3: ImageFormField — EditableComponentProps + contextErrors

Section titled “T-3: ImageFormField — EditableComponentProps + contextErrors”

Source: REQ-FE-044

Changes:

  1. Add contextErrors?: FieldError[] prop
  2. Adopt EditableComponentProps<string | null> interface pattern:
    • Add initialValue as the canonical prop (per EditableComponentProps<T>)
    • Keep imageUrl as a backward-compatible alias, marked with /** @deprecated Use initialValue instead. */
    • Component reads initialValue ?? imageUrl internally
    • Keep onChange callback
  3. Display contextual errors alongside the image field

Tests:

  • contextErrors rendered when provided
  • Existing behavior unchanged when contextErrors omitted
  • initialValue prop works as primary data source
  • imageUrl prop still works (backward compat) but is deprecated

T-4: ItemGridColumns — Expand lookups + image editor wiring

Section titled “T-4: ItemGridColumns — Expand lookups + image editor wiring”

Source: REQ-FE-045

Rationale for lookup expansion: The current ItemGridLookups interface has only 2 fields (supplier, classificationType), but the item grid has 9+ columns with typeahead editors. The 7 missing fields mean those columns have their typeahead data sources hardwired or not wired through the typed provider interface at all. Expanding the interface makes all data dependencies explicit and type-safe (FD-01), and is a prerequisite for Phase 3.6 where the app wires TanStack-backed lookup hooks into the grid — it needs the interface to accept them all.

Changes:

  1. Expand ItemGridLookups interface:
    export interface ItemGridLookups {
    supplier?: (search: string) => Promise<TypeaheadOption[]>;
    classificationType?: (search: string) => Promise<TypeaheadOption[]>;
    classificationSubType?: (search: string) => Promise<TypeaheadOption[]>;
    useCase?: (search: string) => Promise<TypeaheadOption[]>;
    facility?: (search: string) => Promise<TypeaheadOption[]>;
    department?: (search: string) => Promise<TypeaheadOption[]>;
    location?: (search: string) => Promise<TypeaheadOption[]>;
    sublocation?: (search: string) => Promise<TypeaheadOption[]>;
    unit?: (search: string) => Promise<TypeaheadOption[]>;
    }
  2. Add image editor hooks to the column factory:
    export interface ItemGridEditorHooks {
    useImageUpload: () => { mutateAsync: (file: Blob) => Promise<string>; isPending: boolean };
    useCheckReachability: () => { mutateAsync: (url: string) => Promise<boolean> };
    }
    Both fields are required (no ?), consistent with FD-15 and the ImageCellEditorConfig change in T-2. This shifts the missing-hook error from runtime to compile time.
  3. Wire createImageCellEditor in the image column definition using the provided hooks

Tests:

  • Column defs include image cell renderer and editor when hooks provided
  • Column defs work without hooks (Storybook/default mode)

T-5: TypeaheadCellEditor — Evaluate hook-style interface

Section titled “T-5: TypeaheadCellEditor — Evaluate hook-style interface”

Source: goal.md scope item

Evaluation: The current lookup: (search: string) => Promise<TypeaheadOption[]> interface is already a typed callback (FD-01 compliant as a promise-based provider). The component manages its own debounce, loading, and error state from the promise.

Decision to make: Convert to hook-style useLookup: (query: string) => { data, isLoading }, or keep the promise callback?

Recommendation: Keep the current promise callback. Reasons:

  • Already FD-01 compliant (typed, TanStack-agnostic)
  • Component correctly manages loading/error from the promise
  • Debounce is a UI concern that belongs in the component
  • Converting would require the component to re-render on every keystroke via hook state changes, which is the current behavior anyway

If kept as-is, this task is a no-op except for documenting the decision.

Terminal window
make check # lint + typecheck
make test # unit tests
make build # Storybook build

Verify all existing Storybook stories still render correctly with default handlers.

Update session log / byproducts in the documentation worktree.

Terminal window
# In the documentation worktree
make pr-checks

Commit documentation changes referencing Phase 3.3.

  • ImageUploadDialog: indeterminate progress, UploadError state, lifecycle types
  • ImageCellEditor: factory accepts typed provider hooks
  • ImageFormField: accepts contextErrors
  • ItemGridColumns: expanded lookups interface, image editor hook wiring
  • TypeaheadCellEditor: decision documented (keep or convert)
  • All unit tests pass, including new tests for each change
  • All Storybook stories render with default handlers
  • Full local checks pass (ux-prototype)
  • Documentation worktree: make pr-checks passes, changes committed

STOP: Review component changes before proceeding to Phase 3.4 (publish).

#QuestionOptionsRecommendationDecision
1ImageFormField prop naming: imageUrl vs. initialValueA: rename to initialValue (consistent with lifecycle), B: keep imageUrl + add initialValue alias, C: keep imageUrl onlyB (backward compat + new pattern)Agreed, mark the alias as “Deprecated”
2TypeaheadCellEditor interfaceA: convert to hook, B: keep promise callbackB (already FD-01 compliant)Agreed

Copyright: (c) Arda Systems 2025-2026, All rights reserved