Skip to content

refactor(portal-framework-ui, portal-plugin-dashboard): streamline dialog footer rendering and avatar upload component#506

Merged
pcfreak30 merged 3 commits intodevelopfrom
libs/portal-plugin-dashboard
Sep 23, 2025
Merged

refactor(portal-framework-ui, portal-plugin-dashboard): streamline dialog footer rendering and avatar upload component#506
pcfreak30 merged 3 commits intodevelopfrom
libs/portal-plugin-dashboard

Conversation

@pcfreak30
Copy link
Copy Markdown
Member

@pcfreak30 pcfreak30 commented Sep 23, 2025

Dialog System Refactoring:

  • The DialogFooterContent component now receives an explicit onCancel prop instead of relying on internal closeDialog logic
  • The CustomDialog component has simplified rendering by removing automatic action generation
  • The DefaultFooter component no longer generates default dialog actions automatically

Footer Environment System Improvements:

  • Enhanced null safety throughout the footer environment building process with optional chaining
  • The buildFooterEnvironment function now properly distinguishes between standalone and dialog contexts
  • Added separate builder functions for better code organization
  • Improved conditional logic for including form contexts in footers

Avatar Upload Component Rewrite:

  • Completely refactored AvatarUpload to use UploadManagerProvider context system
  • Removed direct Uppy instantiation and event handling
  • Added proper service registration mechanism for avatar uploads
  • Implemented file preview handling with proper object URL cleanup
  • Replaced custom drop zone rendering with standardized Dropzone component

File Upload Status Enhancements:

  • Added new PREPROCESSING status to FileStatus enum
  • Updated FileItem component to display preprocessing status and progress
  • Enhanced progress tracking for preprocessing state

General Improvements:

  • Added numerous optional chaining checks to prevent null/undefined access errors
  • Improved event listener cleanup in upload components
  • Better separation of concerns in dialog and footer components
  • Removed unused imports and code throughout multiple files

Summary by CodeRabbit

  • New Features

    • Dialog footers now accept a custom Cancel handler.
    • Uploads gain a "Preprocessing" status with progress shown in dropzones and file lists.
    • Avatar upload now uses the Upload Manager with live preview and progress UI.
  • Breaking Changes

    • Footers no longer auto-generate default confirm/cancel actions; provide explicit action buttons.
    • Custom dialogs no longer render headers or actions by default.
    • Some footer components no longer accept onConfirm/submitLabel props.
  • Refactor

    • Footer and dialog handling modernized and made resilient across dialog/form/wizard contexts.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Sep 23, 2025

⚠️ No Changeset found

Latest commit: b9c3686

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 23, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Threads an optional onCancel through dialog footer rendering and renderFooter, simplifies footer/action generation with null-safe environment access, migrates AvatarUpload to an UploadManager-driven flow (adds PREPROCESSING status), and exposes default upload configs.

Changes

Cohort / File(s) Summary
Dialog cancel wiring
libs/portal-framework-ui/src/components/dialog/Dialog.renderer.tsx, libs/portal-framework-ui/src/components/dialog/DialogFooterContent.tsx
Add and propagate onCancel into dialog footer rendering; bind footer cancel to existing closeDialog; remove internal action-generation in DialogFooterContent and delegate footer rendering to renderFooter with isDialog/onCancel.
CustomDialog simplification
libs/portal-framework-ui/src/components/dialog/types/CustomDialog.tsx
Remove header/action rendering and related utilities; component now renders only content and retains useForceRerender.
Footer generation & context updates
libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx, .../utils/renderFooter.tsx, .../context/FooterContext.tsx, libs/portal-framework-ui/src/components/actions/*
Make environment access null-safe, add onCancel?: () => void to FooterRenderConfig and thread it through renderFooter; split standalone vs dialog footer environment builders; export createDialogActions; add useOptionalFooterContext.
Footer registry & types
libs/portal-framework-ui/src/components/shared/registry/FooterRegistry.ts, .../registry/types.ts, .../types/footer.ts, .../types/form.ts
Change default form-type mapping to DEFAULT, widen FooterComponent prop union to include FormFooterProps, add FormFooterProps (with onConfirm/submitLabel), and make form type guards null-safe.
Footer implementations
libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx, FormFooter.tsx, StepFormFooter.tsx, WizardFooter.tsx
Simplify footers to rely on provided actionButtons; remove onConfirm/submitLabel from several footers (narrow props via Pick); add/adjust environment usage where needed.
Upload manager & types
libs/portal-plugin-dashboard/src/contexts/UploadManagerContext.tsx, .../features/upload/Manager.ts, .../types/upload.ts
Restrict UploadManager initialization to first creation (ignore subsequent configs), export DEFAULT_MAIN_CONFIG and DEFAULT_AVATAR_CONFIG, prefer CID from response when enriching files, and add FileStatus.PREPROCESSING.
Avatar upload rewrite
libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx, libs/portal-plugin-dashboard/src/ui/dialogs/uploadAvatar.tsx
Replace direct Uppy usage with UploadManager flow: add AvatarUploadInner, AvatarUploadDropzone, register avatar-upload service, subscribe to manager events, manage preview/progress/status, and export default AvatarUpload; update dialog import to default.
Dropzone & file UI updates
libs/portal-plugin-dashboard/src/contexts/DropzoneContext.tsx, libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx, libs/portal-plugin-dashboard/src/ui/components/FileItem.tsx
Minor type/format tweaks in DropzoneContext; Dropzone subscribes to preprocess-progress/preprocess-complete and triggers rerenders; FileItem shows PREPROCESSING and uses preprocess progress values.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Dialog
  participant Footer as DialogFooterContent
  participant Render as renderFooter
  participant Env as FooterEnv

  User->>Dialog: open()
  Dialog->>Footer: render({ onCancel: closeDialog })
  Footer->>Render: renderFooter({ isDialog: true, onCancel })
  Render->>Env: build footer env (container | form | wizard/step)
  alt user clicks cancel
    Env-->>Render: invoke onCancel
    Render-->>Footer: call onCancel
    Footer-->>Dialog: closeDialog()
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant Avatar as AvatarUpload
  participant UM as UploadManager
  participant Svc as avatar-upload service
  participant UI as Dropzone/FileItem

  Avatar->>UM: register/get(serviceId="avatar-upload")
  User->>UI: drop file
  UI->>UM: addFiles()
  UM-->>UI: preprocess-progress / preprocess-complete
  note over UI: show PREPROCESSING state & preprocess progress
  UM->>Svc: upload (plugin pipeline)
  UM-->>UI: upload-progress
  alt success
    UM-->>UI: complete
    UI-->>Avatar: update preview, notify success
  else error
    UM-->>UI: error
    UI-->>Avatar: notify failure
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I’m a rabbit who hops through code and logs,
I threaded onCancel beneath dialog frogs.
Avatars hum as preprocessing spins,
Progress bips and uploads chase their wins.
Carrots, commits, and tiny hopping jigs. 🥕🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title accurately summarizes the primary refactor areas by naming both affected packages and highlighting the two main changes: dialog/footer rendering and the avatar upload component. It is a concise, single sentence that aligns with the PR objectives and the file-level summaries which show focused changes to DialogFooterContent, renderFooter and DefaultFooter alongside a rewrite of AvatarUpload to use the UploadManager. The phrasing is specific and reviewer-friendly, avoiding vague terms or extraneous noise.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04a0779 and b9c3686.

📒 Files selected for processing (3)
  • libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/shared/types/footer.ts (4 hunks)
  • libs/portal-framework-ui/src/components/shared/types/form.ts (4 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
libs/portal-framework-ui/src/components/dialog/Dialog.renderer.tsx (1)

71-84: Fix provider nesting order: comment says reverse, code doesn't reverse.

This currently wraps in forward order; last provider becomes outermost. Reverse the iteration to match the comment and expected nesting.

-  requiredContexts.forEach((contextName) => {
+  requiredContexts.slice().reverse().forEach((contextName) => {
     const ProviderComponent = contextProviders[contextName];
     if (ProviderComponent) {
       dialogContent = (
         <ProviderComponent
           dialog={dialogWithFormType}
           formMethods={formMethods}>
           {dialogContent}
         </ProviderComponent>
       );
     }
   });
libs/portal-plugin-dashboard/src/features/upload/Manager.ts (1)

653-662: Guard responseText access in error handler

responseText may be undefined depending on the transport. Add a type guard.

Apply:

-        if (xhr.status === 507) {
+        if (xhr.status === 507) {
           error.details = "Upload quota exceeded";
-        } else if (xhr.responseText.toLowerCase().includes("is not verified")) {
+        } else if (typeof xhr.responseText === "string" && xhr.responseText.toLowerCase().includes("is not verified")) {
           error.details = "Please verify your email to upload files";
         }
libs/portal-plugin-dashboard/src/contexts/UploadManagerContext.tsx (1)

25-33: Config becomes immutable after first getUploadManager()

newConfig is ignored after initialization, which can break flows that rely on reconfiguring (e.g., avatar vs main). Support re-init on config change or document the required reset.

Apply:

-export function UploadManagerProvider({
+export function UploadManagerProvider({
   children,
   defaultConfig,
 }: UploadManagerProviderProps) {
   const uploadManagerRef = useRef<Manager | null>(null);
-  const config = defaultConfig;
+  const configRef = useRef<UploadManagerConfig | undefined>(defaultConfig);
 
-  const getUploadManager = (newConfig?: UploadManagerConfig) => {
-    if (!uploadManagerRef.current) {
-      uploadManagerRef.current = new Manager(
-        config || newConfig || { type: "main" },
-      );
-    }
-    return uploadManagerRef.current;
-  };
+  const getUploadManager = (newConfig?: UploadManagerConfig) => {
+    const cfg = newConfig || configRef.current || { type: "main" };
+    if (!uploadManagerRef.current) {
+      uploadManagerRef.current = new Manager(cfg);
+      configRef.current = cfg;
+      return uploadManagerRef.current;
+    }
+    // Recreate manager if config changes
+    if (newConfig && JSON.stringify(newConfig) !== JSON.stringify(configRef.current)) {
+      uploadManagerRef.current.reset();
+      uploadManagerRef.current = new Manager(newConfig);
+      configRef.current = newConfig;
+    }
+    return uploadManagerRef.current;
+  };
libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (1)

137-145: onUploadComplete condition likely never true

Uppy files don’t expose a string status matching UploadStatus. This can suppress onUploadComplete.

Apply:

-    if (
-      !uploading &&
-      files.length > 0 &&
-      files.every((f) => f.status === UploadStatus.COMPLETED)
-    ) {
+    if (
+      !uploading &&
+      files.length > 0 &&
+      files.every((f) => f.progress?.uploadComplete)
+    ) {
       onUploadComplete?.();
     }
🧹 Nitpick comments (13)
libs/portal-framework-ui/src/components/shared/types/form.ts (1)

121-123: Align type guard signatures with optional chaining usage.

These guards now tolerate undefined via optional chaining. Make the parameter optional to reflect reality and improve type safety at call sites.

-export function isSimpleForm(
-  ctx: AnyFormEnvironment,
-): ctx is SimpleFormEnvironment {
+export function isSimpleForm(
+  ctx?: AnyFormEnvironment,
+): ctx is SimpleFormEnvironment {
   return ctx?.type === FormType.SIMPLE;
 }
 
-export function isStepForm(
-  ctx: AnyFormEnvironment,
-): ctx is StepFormEnvironment {
+export function isStepForm(
+  ctx?: AnyFormEnvironment,
+): ctx is StepFormEnvironment {
   return ctx?.type === FormType.STEP;
 }
 
-export function isWizardForm(
-  ctx: AnyFormEnvironment,
-): ctx is WizardFormEnvironment {
+export function isWizardForm(
+  ctx?: AnyFormEnvironment,
+): ctx is WizardFormEnvironment {
   return ctx?.type === FormType.WIZARD;
 }

Also applies to: 132-134, 143-145

libs/portal-framework-ui/src/components/shared/registry/FooterRegistry.ts (1)

65-73: Behavior change: SIMPLE/default now map to DEFAULT footer. Verify no regression.

Previously SIMPLE mapped to FORM. With DefaultFooter no longer auto-generating actions, ensure UnifiedFooter still provides submit/cancel as expected for simple forms.

If you intended to keep FORM-specific rendering (styling/behavior), consider retaining SIMPLE -> FORM mapping:

-      switch (context.form?.type) {
+      switch (context.form?.type) {
         case FormType.WIZARD:
           return FooterType.WIZARD_FORM;
         case FormType.STEP:
           return FooterType.STEP_FORM;
         case FormType.SIMPLE:
-        default:
-          return FooterType.DEFAULT;
+          return FooterType.FORM;
+        default:
+          return FooterType.DEFAULT;
       }
libs/portal-framework-ui/src/components/dialog/types/CustomDialog.tsx (1)

19-29: Avoid destructuring unused props to prevent no-unused-vars issues.

Preserve API but destructure only used fields; forward the rest if needed later.

-export function CustomDialog({
-  actions,
-  classNames,
-  content,
-  description,
-  forceRerender,
-  header,
-  onClose,
-  onConfirm,
-  title,
-}: CustomDialogProps) {
+export function CustomDialog(props: CustomDialogProps) {
+  const { classNames, content, forceRerender } = props;
   // Implement forceRerender mechanism
   useForceRerender(forceRerender);
 
   return (
     <>
       <div className={classNames?.content}>{content}</div>
     </>
   );
 }

Also applies to: 33-37

libs/portal-framework-ui/src/components/shared/utils/renderFooter.tsx (1)

23-27: Solid consolidation and onCancel threading. One suggestion: confine hook use to caller.

renderFooter calls a hook (useOptionalStepControlContext). To keep rules-of-hooks happy across linters, consider obtaining stepControl in the caller (component) and passing it in, or wrap renderFooter in a small component that hosts the hook.

Example (caller side change in DialogFooterContent):

-      {renderFooter({
+      {renderFooter({
         className: currentDialog.classNames?.footer,
         dialogConfig: currentDialog,
         footer: currentDialog.footer,
         isDialog: true,
         onCancel,
       })}

Alternative: turn renderFooter into a component (FooterRenderer) and move the logic inside it.

Also applies to: 49-52, 66-68, 85-87, 104-112, 113-121, 123-142, 147-151, 153-156, 176-186

libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (1)

286-290: Remove duplicate footer.actions check.

The check for Array.isArray(config.footer) appears twice; keep one.

-    // Check if the footer config has an actions array
-    if (config && Array.isArray(config.footer)) {
-      baseActions = config.footer;
-    }
...
-    // Check if the footer config has an actions array
-    if (config && Array.isArray(config.footer)) {
-      baseActions = config.footer;
-    }
+    // Check if the footer config has an actions array
+    if (config && Array.isArray(config.footer)) {
+      baseActions = config.footer;
+    }

Also applies to: 320-323

libs/portal-plugin-dashboard/src/features/upload/Manager.ts (3)

53-64: Freeze exported defaults to avoid accidental mutation

These exported objects can be mutated by consumers. Freeze them to enforce immutability.

Apply:

-export const DEFAULT_MAIN_CONFIG: Partial<UploadManagerConfig> = {
+export const DEFAULT_MAIN_CONFIG = Object.freeze<Partial<UploadManagerConfig>>({
   autoProceed: false,
   maxNumberOfFiles: undefined,
   type: UPLOAD_TYPE_MAIN,
-};
+});
 
-export const DEFAULT_AVATAR_CONFIG: Partial<UploadManagerConfig> = {
+export const DEFAULT_AVATAR_CONFIG = Object.freeze<Partial<UploadManagerConfig>>({
   allowedFileTypes: ["image/*"],
   autoProceed: true,
   maxNumberOfFiles: 1,
   type: UPLOAD_TYPE_AVATAR,
-};
+});

39-52: Remove duplicate StorageInfo interface

The interface is declared twice back-to-back. Drop the duplicate.

Apply:

 interface StorageInfo {
   available: number;
   total: number;
   used: number;
   usedPercentage: number;
 }
 
-interface StorageInfo {
-  available: number;
-  total: number;
-  used: number;
-  usedPercentage: number;
-}

514-525: Avoid noisy warnings when sdk is absent

Short‑circuit when no SDK to prevent expected try/catch errors during plugin ID selection.

Apply:

-  async #fetchUploadLimit() {
-    try {
-      const uploadLimitResponse = await this.#sdk!.account().uploadLimit();
+  async #fetchUploadLimit() {
+    if (!this.#sdk) {
+      this.#uploadLimit = null;
+      return;
+    }
+    try {
+      const uploadLimitResponse = await this.#sdk.account().uploadLimit();
       this.#uploadLimit = uploadLimitResponse.data.limit;
     } catch (error) {
       console.warn(
         "Failed to fetch upload limit, defaulting to small file handling:",
         error,
       );
       this.#uploadLimit = null;
     }
   }
libs/portal-plugin-dashboard/src/ui/components/FileItem.tsx (1)

101-110: DRY up progress computation (single source of truth)

Compute progress once and reuse for text and bar to avoid drift.

Apply:

-              {fileStatus !== FileStatus.COMPLETE &&
-                ((file.progress?.percentage !== undefined) || 
-                 (fileStatus === FileStatus.PREPROCESSING && file.progress?.preprocess?.value !== undefined)) && (
-                  <span>
-                    {Math.round(
-                      fileStatus === FileStatus.PREPROCESSING 
-                        ? file.progress?.preprocess?.value || 0
-                        : file.progress?.percentage || 0
-                    )}%
-                  </span>
-                )}
+              {fileStatus !== FileStatus.COMPLETE &&
+                ((file.progress?.percentage !== undefined) ||
+                 (fileStatus === FileStatus.PREPROCESSING && file.progress?.preprocess?.value !== undefined)) && (
+                  <span>{Math.round(progress)}%</span>
+                )}
@@
-            {fileStatus !== FileStatus.COMPLETE &&
-              ((file.progress?.percentage !== undefined) || 
-               (fileStatus === FileStatus.PREPROCESSING && file.progress?.preprocess?.value !== undefined)) && (
-                <Progress
-                  className="mt-1 h-1"
-                  value={
-                    fileStatus === FileStatus.PREPROCESSING 
-                      ? file.progress?.preprocess?.value || 0
-                      : file.progress?.percentage || 0
-                  }
-                />
-              )}
+            {fileStatus !== FileStatus.COMPLETE &&
+              ((file.progress?.percentage !== undefined) ||
+               (fileStatus === FileStatus.PREPROCESSING && file.progress?.preprocess?.value !== undefined)) && (
+                <Progress className="mt-1 h-1" value={progress} />
+              )}

Add near Line 88 (outside diffed lines) to support the change:

// derive progress once
const progress =
  fileStatus === FileStatus.PREPROCESSING
    ? file.progress?.preprocess?.value || 0
    : file.progress?.percentage || 0;

Also applies to: 113-121

libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx (4)

195-223: Minor a11y/UX: respect disabled state, prevent form submit, restrict file picker.

-    <div
+    <div
       aria-label="Upload profile picture. Press Enter, Space, or click to choose a file, or drag and drop an image."
       className="border-muted hover:border-muted/50 rounded-lg border-2 border-dashed p-8 text-center transition-colors"
       onClick={handleFileButtonClick}
       onKeyDown={(e) => {
         if (e.key === "Enter" || e.key === " ") {
           e.preventDefault();
           handleFileButtonClick(e);
         }
       }}
       ref={containerRef}
       role="button"
+      aria-disabled={disabled}
       tabIndex={0}>
       <input
         className="hidden"
         disabled={disabled}
-        id="file-upload-input"
         multiple={multiple}
         onChange={handleFileInput}
         ref={fileInputRef}
         type="file"
+        accept="image/*"
       />
       <Upload className="text-muted-foreground mx-auto mb-4 h-8 w-8" />
       <p className="text-foreground mb-2">Drag and drop your image here</p>
       <p className="text-muted-foreground mb-4 text-sm">or</p>
-      <Button className="bg-secondary text-foreground hover:bg-secondary/60">
+      <Button
+        type="button"
+        onClick={handleFileButtonClick}
+        disabled={disabled}
+        className="bg-secondary text-foreground hover:bg-secondary/60"
+      >
         Choose File
       </Button>

45-47: Rely on provider defaultConfig; avoid redundant type override.

Since the provider supplies avatar defaults, use getUploadManager() without args to reduce config drift.

-  const uploadManager = getUploadManager({
-    type: "avatar",
-  });
+  const uploadManager = getUploadManager();

90-90: Add uploadManager to effect deps (stability, exhaustive-deps).

-  }, [apiUrl]);
+  }, [apiUrl, uploadManager]);

If currentAvatar is intentionally part of the service config (e.g., response parsing), include it too.


137-147: Optional: sync preview when currentAvatar prop changes.

Keeps the UI in sync if the parent updates the avatar URL externally.

Add this effect near the preview state:

useEffect(() => {
  if (!preview || !preview.startsWith("blob:")) {
    setPreview(currentAvatar ?? null);
  }
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentAvatar]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f5de9e and 5ba9975.

📒 Files selected for processing (16)
  • libs/portal-framework-ui/src/components/dialog/Dialog.renderer.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/dialog/DialogFooterContent.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/dialog/types/CustomDialog.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (13 hunks)
  • libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/shared/registry/FooterRegistry.ts (1 hunks)
  • libs/portal-framework-ui/src/components/shared/types/form.ts (4 hunks)
  • libs/portal-framework-ui/src/components/shared/utils/renderFooter.tsx (6 hunks)
  • libs/portal-plugin-dashboard/src/contexts/DropzoneContext.tsx (3 hunks)
  • libs/portal-plugin-dashboard/src/contexts/UploadManagerContext.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/features/upload/Manager.ts (3 hunks)
  • libs/portal-plugin-dashboard/src/types/upload.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (5 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/FileItem.tsx (3 hunks)
  • libs/portal-plugin-dashboard/src/ui/dialogs/uploadAvatar.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (10)
libs/portal-framework-ui/src/components/shared/registry/FooterRegistry.ts (1)
go/portal-dashboard/build/static/js/index-DVLEiUlP.js (2)
  • FormType (710-724)
  • FooterType (1541-1549)
libs/portal-plugin-dashboard/src/contexts/UploadManagerContext.tsx (1)
libs/portal-plugin-dashboard/src/features/upload/Manager.ts (1)
  • UploadManagerConfig (28-35)
libs/portal-plugin-dashboard/src/ui/components/FileItem.tsx (1)
go/portal-plugin-dashboard/build/static/js/UploadProgress-DV-AG4es.js (1)
  • fileStatus (366-366)
libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx (6)
libs/portal-plugin-dashboard/src/contexts/UploadManagerContext.tsx (2)
  • useUploadManagerContext (55-63)
  • UploadManagerProvider (20-53)
libs/portal-framework-ui/src/hooks/useApiUrl.ts (1)
  • useApiUrl (3-24)
libs/portal-plugin-dashboard/src/features/upload/Manager.ts (6)
  • file (449-486)
  • file (527-587)
  • file (589-595)
  • file (597-599)
  • file (601-603)
  • DEFAULT_AVATAR_CONFIG (59-64)
libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (1)
  • Dropzone (61-81)
libs/portal-plugin-dashboard/src/contexts/DropzoneContext.tsx (1)
  • useDropzoneContext (233-241)
go/portal-plugin-dashboard/build/static/js/UploadProgress-DV-AG4es.js (3)
  • handleFileButtonClick (226-234)
  • handleFileInput (198-213)
  • fileInputRef (165-165)
libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (3)
go/portal-plugin-dashboard/build/static/js/UploadProgress-DV-AG4es.js (5)
  • handleDirectoryButtonClick (235-243)
  • defaultRenderDropZone (562-628)
  • isDragOver (163-163)
  • handleFileButtonClick (226-234)
  • handleRemoveFile (512-516)
libs/portal-plugin-dashboard/src/features/upload/Manager.ts (5)
  • file (449-486)
  • file (527-587)
  • file (589-595)
  • file (597-599)
  • file (601-603)
libs/portal-plugin-dashboard/src/ui/components/FileItem.tsx (1)
  • FileItem (29-138)
libs/portal-framework-ui/src/components/shared/utils/renderFooter.tsx (2)
go/portal-dashboard/build/static/js/index-DVLEiUlP.js (2)
  • Environment (932-939)
  • hasStepControl (2267-2267)
libs/portal-framework-ui/src/components/shared/types/form.ts (1)
  • isWizardForm (140-144)
libs/portal-framework-ui/src/components/dialog/DialogFooterContent.tsx (1)
libs/portal-framework-ui/src/components/shared/utils/renderFooter.tsx (1)
  • renderFooter (41-97)
libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (1)
libs/portal-framework-ui/src/components/shared/types/form.ts (3)
  • isWizardForm (140-144)
  • isStepForm (129-133)
  • isSimpleForm (118-122)
libs/portal-plugin-dashboard/src/features/upload/Manager.ts (1)
libs/portal-plugin-dashboard/src/types/upload.ts (1)
  • UPLOAD_TYPE_MAIN (24-24)
libs/portal-framework-ui/src/components/shared/types/form.ts (1)
go/portal-dashboard/build/static/js/index-DVLEiUlP.js (1)
  • FormType (710-724)
🪛 Biome (2.1.2)
libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx

[error] 176-176: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 199-199: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🔇 Additional comments (16)
libs/portal-framework-ui/src/components/shared/types/form.ts (1)

12-14: Formatting-only change looks good.

libs/portal-framework-ui/src/components/dialog/Dialog.renderer.tsx (1)

64-67: Good: explicit cancel wiring into the footer.

Passing onCancel={closeDialog} makes the cancel path explicit and predictable.

libs/portal-framework-ui/src/components/dialog/DialogFooterContent.tsx (1)

7-10: LGTM: Footer delegated to renderFooter with onCancel and dialog context.

Null-guard for form dialogs avoids duplicate actions; onCancel plumbed correctly.

Also applies to: 12-15, 21-31

libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (1)

15-18: LGTM: Renders only provided actions; no implicit defaults.

This aligns with the new centralized action generation.

libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (1)

345-356: onClose propagation looks correct; confirm consumers no longer rely on onConfirm in DefaultFooter.

DefaultFooter dropped onConfirm; ensure other footer types (Form/Step/Wizard) handle onConfirm as needed.

libs/portal-plugin-dashboard/src/features/upload/Manager.ts (1)

470-476: CID fallback looks good

The meta fallback for CID is correct and safe.

libs/portal-plugin-dashboard/src/types/upload.ts (1)

31-33: PREPROCESSING status addition: LGTM

Works with the new UI flow. Please confirm all status consumers handle PREPROCESSING (e.g., badges/progress).

libs/portal-plugin-dashboard/src/ui/components/FileItem.tsx (2)

92-92: Badge text for PREPROCESSING: LGTM


163-166: PREPROCESSING detection order is sensible

Preprocess is checked after uploadStarted, which aligns with Uppy’s lifecycle. Fine as-is.

libs/portal-plugin-dashboard/src/contexts/DropzoneContext.tsx (3)

12-12: Type import for UppyFileDefault: LGTM


38-41: Signature formatting change: LGTM


125-129: Input reset after directory select: LGTM

libs/portal-plugin-dashboard/src/ui/dialogs/uploadAvatar.tsx (1)

3-3: Default import switch for AvatarUpload

Confirm the component is exported as default at "@/ui/components/AvatarUpload".

libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (2)

183-197: Preprocess event wiring: LGTM

Correctly triggers re-renders during preprocessing and cleans up listeners.


211-218: renderDropZone signature update: LGTM

libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx (1)

92-110: Scope progress/status to this service (avoid cross-component bleed).

If UploadManager aggregates events across services, this component might react to uploads from other dropzones. Prefer service-scoped events/queries (e.g., on("upload-progress", { serviceId }), getUploadProgress(serviceId)).

@pcfreak30 pcfreak30 force-pushed the libs/portal-plugin-dashboard branch from 5ba9975 to b291d50 Compare September 23, 2025 21:15
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
libs/portal-framework-ui/src/components/shared/utils/renderFooter.tsx (2)

41-97: Fix Rules of Hooks: renderFooter calls hooks outside a component.

useOptionalStepControlContext is invoked in a plain function; move hook calls into a component and keep a thin wrapper for backward compatibility.

-export function renderFooter<T extends BaseRecord = any>(
-  config: FooterRenderConfig<T>,
-): React.ReactNode {
-  const {
-    className,
-    dialogConfig,
-    footer,
-    formMethods,
-    isDialog = false,
-    onCancel,
-    unifiedFooterConfig,
-  } = config;
-
-  // If footer is explicitly false, don't render anything
-  if (footer === false) {
-    return null;
-  }
-
-  // Handle function-based footers
-  if (footer && isDialogFooterFunction(footer)) {
-    const stepControl = useOptionalStepControlContext();
-    const footerEnvironment = buildFooterEnvironment({
-      dialogConfig,
-      formMethods,
-      isDialog,
-      onCancel,
-      stepControl,
-    });
-
-    const customFooter = footer(footerEnvironment);
-    return <DialogFooter className={className}>{customFooter}</DialogFooter>;
-  }
-
-  // Handle static footer content
-  if (footer) {
-    return <DialogFooter className={className}>{footer}</DialogFooter>;
-  }
-
-  // Default: Use UnifiedFooter system
-  const stepControl = useOptionalStepControlContext();
-  const footerEnvironment = buildFooterEnvironment({
-    dialogConfig,
-    formMethods,
-    isDialog,
-    onCancel,
-    stepControl,
-  });
-
-  return (
-    <DialogFooter className={className}>
-      <UnifiedFooter
-        config={unifiedFooterConfig || dialogConfig?.formConfig || dialogConfig}
-        environment={footerEnvironment}
-      />
-    </DialogFooter>
-  );
-}
+export function renderFooter<T extends BaseRecord = any>(
+  config: FooterRenderConfig<T>,
+): React.ReactNode {
+  return <FooterRenderer {...config} />;
+}
+
+function FooterRenderer<T extends BaseRecord = any>(
+  props: FooterRenderConfig<T>,
+): React.ReactElement | null {
+  const {
+    className,
+    dialogConfig,
+    footer,
+    formMethods,
+    isDialog = false,
+    onCancel,
+    unifiedFooterConfig,
+  } = props;
+
+  const stepControl = useOptionalStepControlContext();
+
+  // If footer is explicitly false, don't render anything
+  if (footer === false) {
+    return null;
+  }
+
+  // Handle function-based footers
+  if (footer && isDialogFooterFunction(footer)) {
+    const footerEnvironment = buildFooterEnvironment({
+      dialogConfig,
+      formMethods,
+      isDialog,
+      onCancel,
+      stepControl,
+    });
+    const customFooter = footer(footerEnvironment);
+    return <DialogFooter className={className}>{customFooter}</DialogFooter>;
+  }
+
+  // Handle static footer content
+  if (footer) {
+    return <DialogFooter className={className}>{footer}</DialogFooter>;
+  }
+
+  // Default: Use UnifiedFooter system
+  const footerEnvironment = buildFooterEnvironment({
+    dialogConfig,
+    formMethods,
+    isDialog,
+    onCancel,
+    stepControl,
+  });
+
+  return (
+    <DialogFooter className={className}>
+      <UnifiedFooter
+        config={unifiedFooterConfig || dialogConfig?.formConfig || dialogConfig}
+        environment={footerEnvironment}
+      />
+    </DialogFooter>
+  );
+}

8-8: Remove unused import.

isStepFormConfig is not used in this module.

-import { isStepFormConfig } from "../../form/types";
libs/portal-plugin-dashboard/src/contexts/DropzoneContext.tsx (1)

206-210: Fix missing cleanup for drag/drop event listeners (memory leak).

Return the cleanup function from useEffect so listeners are removed on unmount/deps change.

-  React.useEffect(() => {
-    setupDragAndDropListeners();
-  }, [setupDragAndDropListeners]);
+  React.useEffect(() => {
+    return setupDragAndDropListeners();
+  }, [setupDragAndDropListeners]);
🧹 Nitpick comments (7)
libs/portal-framework-ui/src/components/dialog/DialogFooterContent.tsx (1)

22-30: Return renderFooter directly (remove unnecessary fragment).

Slightly cleaner; no behavior change.

-  return (
-    <>
-      {renderFooter({
-        className: currentDialog.classNames?.footer,
-        dialogConfig: currentDialog,
-        footer: currentDialog.footer,
-        isDialog: true,
-        onCancel,
-      })}
-    </>
-  );
+  return renderFooter({
+    className: currentDialog.classNames?.footer,
+    dialogConfig: currentDialog,
+    footer: currentDialog.footer,
+    isDialog: true,
+    onCancel,
+  });
libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (1)

320-323: Remove duplicate config.footer check.

This block duplicates the earlier check; safe to delete.

-    // Check if the footer config has an actions array
-    if (config && Array.isArray(config.footer)) {
-      baseActions = config.footer;
-    }
libs/portal-plugin-dashboard/src/contexts/DropzoneContext.tsx (2)

222-223: Include PREPROCESSING in “uploading” semantic (better UX).

Treat PREPROCESSING as an active upload phase.

-    uploading: uploadManager.getUploadStatus() === UploadStatus.UPLOADING,
+    uploading: [UploadStatus.PREPROCESSING, UploadStatus.UPLOADING].includes(
+      uploadManager.getUploadStatus(),
+    ),

48-50: Avoid stale “uploading” in context (provider doesn’t re-render on status changes).

Either expose a getter (e.g., getUploadStatus) or subscribe to uploadManager events here to update context on status changes; otherwise consumers can read stale uploading.

If you prefer keeping the boolean, consider subscribing to "preprocess-progress", "upload-progress", "complete", and "error" inside the provider to trigger re-renders.

libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (1)

257-269: Optional: add type="button" for safety in forms.

Prevents accidental form submission in nested form contexts.

-          <Button
+          <Button
             disabled={disabled}
             onClick={handleFileButtonClick}
-            type="button">
+            type="button">
...
-            <Button
+            <Button
               disabled={disabled}
               onClick={handleDirectoryButtonClick}
-              type="button"
+              type="button"
               variant="secondary">
libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx (2)

92-120: Reflect PREPROCESSING in AvatarUpload by subscribing to preprocess events.

Without these, uploadStatus won’t enter PREPROCESSING, so the new UI path won’t render.

   useEffect(() => {
     const unsubscribeProgress = uploadManager.on("upload-progress", () => {
       setUploadProgress(uploadManager.getUploadProgress());
       setUploadStatus(uploadManager.getUploadStatus());
     });

+    const unsubscribePreprocessProgress = uploadManager.on(
+      "preprocess-progress",
+      () => {
+        setUploadProgress(uploadManager.getUploadProgress());
+        setUploadStatus(UploadStatus.PREPROCESSING);
+      },
+    );
+
+    const unsubscribePreprocessComplete = uploadManager.on(
+      "preprocess-complete",
+      () => {
+        setUploadProgress(uploadManager.getUploadProgress());
+        setUploadStatus(uploadManager.getUploadStatus());
+      },
+    );
+
     const unsubscribeComplete = uploadManager.on("complete", (result) => {
       if (result.successful.length > 0) {
         setUploadStatus(uploadManager.getUploadStatus());
         open?.({
           message: "Profile Updated",
           description: "Your profile picture has been updated successfully",
           type: "success",
         });
         onSuccess();
       }
       // Reset the uploader after successful upload
       uploadManager.clearFiles();
     });

     const unsubscribeError = uploadManager.on("error", () => {
       setUploadStatus(uploadManager.getUploadStatus());
       open?.({
         message: "Upload Error",
         description: "Failed to upload profile picture. Please try again.",
         type: "error",
       });
     });

     return () => {
       unsubscribeProgress();
+      unsubscribePreprocessProgress();
+      unsubscribePreprocessComplete();
       unsubscribeComplete();
       unsubscribeError();
     };
-  }, [onSuccess, uploadManager]);
+  }, [onSuccess, uploadManager, open]);

Also applies to: 121-126


218-220: Optional: set type="button" on the CTA.

Prevents unintended form submits if nested in a form.

-      <Button className="bg-secondary text-foreground hover:bg-secondary/60">
+      <Button type="button" className="bg-secondary text-foreground hover:bg-secondary/60">
         Choose File
       </Button>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ba9975 and b291d50.

📒 Files selected for processing (16)
  • libs/portal-framework-ui/src/components/dialog/Dialog.renderer.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/dialog/DialogFooterContent.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/dialog/types/CustomDialog.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (13 hunks)
  • libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/shared/registry/FooterRegistry.ts (1 hunks)
  • libs/portal-framework-ui/src/components/shared/types/form.ts (4 hunks)
  • libs/portal-framework-ui/src/components/shared/utils/renderFooter.tsx (6 hunks)
  • libs/portal-plugin-dashboard/src/contexts/DropzoneContext.tsx (3 hunks)
  • libs/portal-plugin-dashboard/src/contexts/UploadManagerContext.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/features/upload/Manager.ts (3 hunks)
  • libs/portal-plugin-dashboard/src/types/upload.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (5 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/FileItem.tsx (3 hunks)
  • libs/portal-plugin-dashboard/src/ui/dialogs/uploadAvatar.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • libs/portal-plugin-dashboard/src/ui/components/FileItem.tsx
  • libs/portal-plugin-dashboard/src/types/upload.ts
  • libs/portal-framework-ui/src/components/shared/registry/FooterRegistry.ts
  • libs/portal-plugin-dashboard/src/contexts/UploadManagerContext.tsx
  • libs/portal-plugin-dashboard/src/features/upload/Manager.ts
  • libs/portal-framework-ui/src/components/dialog/types/CustomDialog.tsx
  • libs/portal-framework-ui/src/components/shared/types/form.ts
  • libs/portal-plugin-dashboard/src/ui/dialogs/uploadAvatar.tsx
  • libs/portal-framework-ui/src/components/dialog/Dialog.renderer.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
libs/portal-framework-ui/src/components/dialog/DialogFooterContent.tsx (1)
libs/portal-framework-ui/src/components/shared/utils/renderFooter.tsx (1)
  • renderFooter (41-97)
libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx (5)
libs/portal-plugin-dashboard/src/contexts/UploadManagerContext.tsx (2)
  • useUploadManagerContext (55-63)
  • UploadManagerProvider (20-53)
libs/portal-framework-ui/src/hooks/useApiUrl.ts (1)
  • useApiUrl (3-24)
libs/portal-plugin-dashboard/src/features/upload/Manager.ts (1)
  • DEFAULT_AVATAR_CONFIG (59-64)
libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (1)
  • Dropzone (61-81)
libs/portal-plugin-dashboard/src/contexts/DropzoneContext.tsx (1)
  • useDropzoneContext (233-241)
libs/portal-framework-ui/src/components/shared/utils/renderFooter.tsx (1)
go/portal-dashboard/build/static/js/index-DVLEiUlP.js (1)
  • Environment (932-939)
libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (2)
go/portal-plugin-dashboard/build/static/js/UploadProgress-DV-AG4es.js (2)
  • handleDirectoryButtonClick (235-243)
  • handleFileButtonClick (226-234)
libs/portal-plugin-dashboard/src/ui/components/FileItem.tsx (1)
  • FileItem (29-138)
libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (1)
libs/portal-framework-ui/src/components/shared/types/form.ts (3)
  • isWizardForm (140-144)
  • isStepForm (129-133)
  • isSimpleForm (118-122)
🪛 Biome (2.1.2)
libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx

[error] 176-176: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 199-199: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🔇 Additional comments (10)
libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (1)

15-15: LGTM — DefaultFooter now relies on explicit actionButtons.

Change is correct and aligns with the new explicit-footer approach.

libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (2)

176-176: Fix conditional hook calls (Rules of Hooks).

Call hooks unconditionally and handle undefined inside the hook.

-  config && useEnvironmentSync(environment, config.environmentSync);
+  useEnvironmentSync(environment, config?.environmentSync);
...
-  config && useEnvironmentSync(environment, config.environmentSync);
+  useEnvironmentSync(environment, config?.environmentSync);

Also applies to: 199-199


117-122: createDialogActions usage is valid.
createDialogActions accepts type: "alert" | "confirm" | "form" and forwards cancelLabel to createActionHelpers.cancel, so using type: "confirm" and cancelLabel: submitLabel is correct.

libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (3)

183-197: Nice: preprocess events wired with proper cleanup.

Good addition for PREPROCESSING UI updates; cleanup is thorough.

Also applies to: 205-206


43-49: Signature consistency for handleDirectoryButtonClick.

Prop and default renderer signatures are consistent. No action needed.

Also applies to: 215-218


283-302: Passing defaultProps into custom renderer is clear and extensible.

This keeps render overrides ergonomic and future-proof.

libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx (4)

159-166: Good: PREPROCESSING included in progress view.

Matches the new status and aligns with Dropzone’s event wiring.


237-244: Good: defaultConfig shape fixed for UploadManagerProvider.

Passing DEFAULT_AVATAR_CONFIG directly is correct.


60-63: Guard URL parsing when apiUrl is empty/invalid (prevents runtime error).

new URL(apiUrl) throws on empty/invalid strings. Safely parse before use.

-      const apiDomain = new URL(apiUrl).hostname;
-      const apiProto = new URL(apiUrl).protocol;
-      const endpoint = `${apiProto}//account.${apiDomain}/api/account/avatar`;
+      if (!apiUrl) {
+        console.warn("AvatarUpload: missing apiUrl; skipping service registration");
+        return;
+      }
+      let parsedUrl: URL;
+      try {
+        parsedUrl = new URL(apiUrl);
+      } catch {
+        console.warn(`AvatarUpload: invalid apiUrl "${apiUrl}"`);
+        return;
+      }
+      const apiDomain = parsedUrl.hostname;
+      const apiProto = parsedUrl.protocol;
+      const endpoint = `${apiProto}//account.${apiDomain}/api/account/avatar`;

69-76: Parse XHR response in getResponseData (string return breaks uploader).

Return a parsed object from xhr.responseText.

-            getResponseData() {
-              return JSON.stringify({
-                url: currentAvatar,
-              });
-            },
+            // Parse server response (expects JSON).
+            getResponseData(xhr: XMLHttpRequest) {
+              try {
+                return JSON.parse(xhr.responseText || "{}");
+              } catch {
+                return {};
+              }
+            },

…alog footer rendering and avatar upload component

Dialog System Refactoring:
- The DialogFooterContent component now receives an explicit onCancel prop instead of relying on internal closeDialog logic
- The CustomDialog component has simplified rendering by removing automatic action generation
- The DefaultFooter component no longer generates default dialog actions automatically

Footer Environment System Improvements:
- Enhanced null safety throughout the footer environment building process with optional chaining
- The buildFooterEnvironment function now properly distinguishes between standalone and dialog contexts
- Added separate builder functions for better code organization
- Improved conditional logic for including form contexts in footers

Avatar Upload Component Rewrite:
- Completely refactored AvatarUpload to use UploadManagerProvider context system
- Removed direct Uppy instantiation and event handling
- Added proper service registration mechanism for avatar uploads
- Implemented file preview handling with proper object URL cleanup
- Replaced custom drop zone rendering with standardized Dropzone component

File Upload Status Enhancements:
- Added new PREPROCESSING status to FileStatus enum
- Updated FileItem component to display preprocessing status and progress
- Enhanced progress tracking for preprocessing state

General Improvements:
- Added numerous optional chaining checks to prevent null/undefined access errors
- Improved event listener cleanup in upload components
- Better separation of concerns in dialog and footer components
- Removed unused imports and code throughout multiple files
@pcfreak30 pcfreak30 force-pushed the libs/portal-plugin-dashboard branch from b291d50 to bb49076 Compare September 23, 2025 21:55
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
libs/portal-plugin-dashboard/src/features/upload/Manager.ts (2)

605-615: Assign correct uploader plugin on DropTarget file add (use suffixed plugin id).

Patching plugins to serviceId alone likely won’t match registered uploader plugin ids (which are suffixed with -small/-large). Compute the proper plugin id based on file size.

Apply:

-    this.addEvent("file-added", (file: UppyFileDefault) => {
-      // If no plugins are associated with the file yet, associate it with the drop target service
-      if (!file?.plugins?.length && this.#dropTargetServiceId) {
-        this.patchFilesState({
-          [file.id]: {
-            plugins: [this.#dropTargetServiceId],
-          },
-        });
-      }
-    });
+    this.addEvent("file-added", (file: UppyFileDefault) => {
+      if (!file?.plugins?.length && this.#dropTargetServiceId) {
+        const serviceId = this.#dropTargetServiceId;
+        const dataFile = (file as any).data as File | undefined;
+        const setPlugins = (pluginId: string) =>
+          this.patchFilesState({ [file.id]: { plugins: [pluginId] } });
+        if (dataFile) {
+          this.getFilePluginId(dataFile, serviceId)
+            .then(setPlugins)
+            .catch(() => setPlugins(`${serviceId}${SMALL_PLUGIN_SUFFIX}`));
+        } else {
+          setPlugins(`${serviceId}${SMALL_PLUGIN_SUFFIX}`);
+        }
+      }
+    });

653-662: Guard xhr.responseText before toLowerCase to avoid runtime error.

responseText can be null/undefined. This path is hit during error handling; don’t throw here.

Apply:

-        } else if (xhr.responseText.toLowerCase().includes("is not verified")) {
+        } else if (
+          typeof xhr.responseText === "string" &&
+          xhr.responseText.toLowerCase().includes("is not verified")
+        ) {
           error.details = "Please verify your email to upload files";
         }
libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (1)

21-59: Add missing onValidationError prop to interface (fix TS error).

You pass props.onValidationError into DropzoneProvider but it’s not declared in DropzoneProps.

Apply:

 interface DropzoneProps {
   allowedFileTypes?: string[];
   allowFolders?: boolean;
   alwaysShowRemoveButton?: boolean;
   disabled?: boolean;
   dragLeaveClassName?: string;
   dragOverClassName?: string;
   dropZoneClassName?: string;
   fileItemClassName?: string;
   fileListHeader?: React.ReactNode;
   hideStatusIndicators?: boolean;
   maxFileSize?: number;
   maxNumberOfFiles?: number;
   multiple?: boolean;
   onDragLeave?: (event: DragEvent) => void;
   onDragOver?: (event: DragEvent) => void;
   onDrop?: (event: DragEvent) => void;
+  onValidationError?: DropzoneConfig["onValidationError"];
   onFileRemove?: (id: string) => void;
   onFilesChange?: (files: File[]) => void;
   onUploadComplete?: () => void;
   onUploadStart?: () => void;
🧹 Nitpick comments (10)
libs/portal-framework-ui/src/components/dialog/types/CustomDialog.tsx (1)

19-29: Prune unused destructured props for clarity.

actions, description, header, onClose, onConfirm, and title aren’t used anymore. Destructure only what’s needed to avoid confusion and accidental shadowing.

-export function CustomDialog({
-  actions,
-  classNames,
-  content,
-  description,
-  forceRerender,
-  header,
-  onClose,
-  onConfirm,
-  title,
-}: CustomDialogProps) {
+export function CustomDialog({
+  classNames,
+  content,
+  forceRerender,
+}: CustomDialogProps) {
libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (1)

286-287: Remove duplicated footer-array check.

config.footer array handling appears twice. Keep one to simplify flow.

-    // Check if the footer config has an actions array
-    if (config && Array.isArray(config.footer)) {
-      baseActions = config.footer;
-    }
@@
-    // Check if the footer config has an actions array
-    if (config && Array.isArray(config.footer)) {
-      baseActions = config.footer;
-    }
+    // Check if the footer config has an actions array
+    if (config && Array.isArray(config.footer)) {
+      baseActions = config.footer;
+    }

(Apply the block only once; remove the later duplicate at lines 319-322.)

Also applies to: 319-322

libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (2)

15-15: Guard against undefined actions.

If actionButtons can be absent, pass an empty array to avoid renderer errors.

-      actions={actionButtons}
+      actions={actionButtons ?? []}

17-17: Align naming with onCancel across footers.

Consider renaming ActionListRenderer’s prop from closeDialog to onCancel and pass it through for consistency with the rest of the PR.

-      closeDialog={onClose}
+      onCancel={onClose}

Note: This requires updating ActionListRenderer’s prop name accordingly.

libs/portal-plugin-dashboard/src/features/upload/Manager.ts (4)

46-52: Remove duplicate StorageInfo interface.

StorageInfo is declared twice with identical shape. Drop the duplicate to avoid confusion and potential future drift.

Apply:

-interface StorageInfo {
-  available: number;
-  total: number;
-  used: number;
-  usedPercentage: number;
-}

186-194: Avoid needless upload-limit fetch when SDK is absent.

When sdk is undefined (e.g., avatar flow), calling #fetchUploadLimit triggers a warning every time. Short-circuit to the small plugin.

Apply:

-    if (this.#uploadLimit === null) {
-      await this.#fetchUploadLimit();
+    if (this.#uploadLimit === null) {
+      if (!this.#sdk) {
+        return `${serviceId}${SMALL_PLUGIN_SUFFIX}`;
+      }
+      await this.#fetchUploadLimit();
       // If still null after fetching, default to small file handling
       if (this.#uploadLimit === null) {
         return `${serviceId}${SMALL_PLUGIN_SUFFIX}`;
       }
     }

617-633: Also capture upper-case “CID” in complete hook.

Align with #enhanceFileData by handling CID|cid and both body and response root.

Apply:

-      result.successful.forEach((file) => {
-        const cid = file.response?.body?.cid;
-        if (cid) {
+      result.successful.forEach((file) => {
+        const cid =
+          file.response?.body?.cid ??
+          file.response?.body?.CID ??
+          (file.response as any)?.cid ??
+          (file.response as any)?.CID;
+        if (cid) {
           this.patchFilesState({
             [file.id]: {
               meta: {
                 ...file.meta,
                 cid: cid,
               },
             },
           });
         }
       });

266-285: Expose PREPROCESSING via getUploadStatus for UI consistency.

Surface PREPROCESSING when any file is in preprocess stage and upload hasn’t started.

Apply:

   public getUploadStatus(): UploadStatusType {
-    const { totalProgress } = this.#uppy.getState();
+    const { totalProgress } = this.#uppy.getState();
+    const files = this.#uppy.getFiles();
 
     // Check if there are any files with errors
-    const hasErrors = this.#uppy.getFiles().some((file) => file.error);
+    const hasErrors = files.some((file) => file.error);
 
     if (hasErrors) {
       return UploadStatus.ERROR;
     }
 
+    // Any file in preprocessing (before upload start)?
+    const isPreprocessing = files.some(
+      (f) => (f as any).progress?.preprocess && !(f as any).progress?.uploadStarted,
+    );
+    if (isPreprocessing) {
+      return UploadStatus.PREPROCESSING as any;
+    }
+
     if (totalProgress === 0) {
       return UploadStatus.PENDING;
     }
libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx (2)

92-126: Reflect PREPROCESSING in AvatarUpload status (subscribe to preprocess events).

Without this, PREPROCESSING won’t switch the view to UploadProgress.

Apply:

   useEffect(() => {
-    const unsubscribeProgress = uploadManager.on("upload-progress", () => {
+    const unsubscribeProgress = uploadManager.on("upload-progress", () => {
       setUploadProgress(uploadManager.getUploadProgress());
       setUploadStatus(uploadManager.getUploadStatus());
     });
+    const unsubscribePreProgress = uploadManager.on("preprocess-progress", () => {
+      setUploadStatus(UploadStatus.PREPROCESSING);
+    });
+    const unsubscribePreComplete = uploadManager.on("preprocess-complete", () => {
+      // Re-evaluate status; may still be PENDING until upload starts
+      setUploadStatus(uploadManager.getUploadStatus());
+    });
 
     const unsubscribeComplete = uploadManager.on("complete", (result) => {
       if (result.successful.length > 0) {
         setUploadStatus(uploadManager.getUploadStatus());
         open?.({
           message: "Profile Updated",
           description: "Your profile picture has been updated successfully",
           type: "success",
         });
         onSuccess();
       }
       // Reset the uploader after successful upload
       uploadManager.clearFiles();
     });
 
     const unsubscribeError = uploadManager.on("error", () => {
       setUploadStatus(uploadManager.getUploadStatus());
       open?.({
         message: "Upload Error",
         description: "Failed to upload profile picture. Please try again.",
         type: "error",
       });
     });
 
     return () => {
       unsubscribeProgress();
+      unsubscribePreProgress();
+      unsubscribePreComplete();
       unsubscribeComplete();
       unsubscribeError();
     };
   }, [onSuccess, uploadManager]);

167-176: Pass disabled/multiple to AvatarUploadDropzone from renderDropZone defaultProps.

Keeps the custom dropzone in sync with provided flags.

Apply:

-          renderDropZone={() => <AvatarUploadDropzone />}
+          renderDropZone={(_isDragOver, _fileBtn, _dirBtn, defaultProps) => (
+            <AvatarUploadDropzone
+              disabled={!!defaultProps?.disabled}
+              multiple={!!defaultProps?.multiple}
+            />
+          )}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b291d50 and bb49076.

📒 Files selected for processing (16)
  • libs/portal-framework-ui/src/components/dialog/Dialog.renderer.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/dialog/DialogFooterContent.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/dialog/types/CustomDialog.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (13 hunks)
  • libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/shared/registry/FooterRegistry.ts (1 hunks)
  • libs/portal-framework-ui/src/components/shared/types/form.ts (4 hunks)
  • libs/portal-framework-ui/src/components/shared/utils/renderFooter.tsx (6 hunks)
  • libs/portal-plugin-dashboard/src/contexts/DropzoneContext.tsx (3 hunks)
  • libs/portal-plugin-dashboard/src/contexts/UploadManagerContext.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/features/upload/Manager.ts (3 hunks)
  • libs/portal-plugin-dashboard/src/types/upload.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (5 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/FileItem.tsx (3 hunks)
  • libs/portal-plugin-dashboard/src/ui/dialogs/uploadAvatar.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • libs/portal-plugin-dashboard/src/types/upload.ts
  • libs/portal-framework-ui/src/components/shared/registry/FooterRegistry.ts
  • libs/portal-plugin-dashboard/src/contexts/UploadManagerContext.tsx
  • libs/portal-framework-ui/src/components/dialog/Dialog.renderer.tsx
  • libs/portal-plugin-dashboard/src/contexts/DropzoneContext.tsx
  • libs/portal-framework-ui/src/components/shared/types/form.ts
  • libs/portal-plugin-dashboard/src/ui/dialogs/uploadAvatar.tsx
  • libs/portal-plugin-dashboard/src/ui/components/FileItem.tsx
🧰 Additional context used
🧬 Code graph analysis (6)
libs/portal-framework-ui/src/components/shared/utils/renderFooter.tsx (2)
go/portal-dashboard/build/static/js/index-DVLEiUlP.js (2)
  • Environment (932-939)
  • hasStepControl (2267-2267)
libs/portal-framework-ui/src/components/shared/types/form.ts (1)
  • isWizardForm (140-144)
libs/portal-plugin-dashboard/src/features/upload/Manager.ts (1)
libs/portal-plugin-dashboard/src/types/upload.ts (1)
  • UPLOAD_TYPE_MAIN (24-24)
libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (2)
go/portal-plugin-dashboard/build/static/js/UploadProgress-DV-AG4es.js (5)
  • handleDirectoryButtonClick (235-243)
  • defaultRenderDropZone (562-628)
  • isDragOver (163-163)
  • handleFileButtonClick (226-234)
  • handleRemoveFile (512-516)
libs/portal-plugin-dashboard/src/ui/components/FileItem.tsx (1)
  • FileItem (29-138)
libs/portal-framework-ui/src/components/dialog/DialogFooterContent.tsx (1)
libs/portal-framework-ui/src/components/shared/utils/renderFooter.tsx (1)
  • renderFooter (41-97)
libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (2)
libs/portal-framework-ui/src/components/shared/types/form.ts (3)
  • isWizardForm (140-144)
  • isStepForm (129-133)
  • isSimpleForm (118-122)
go/portal-dashboard/build/static/js/index-DVLEiUlP.js (1)
  • submitLabel (2085-2113)
libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx (5)
libs/portal-plugin-dashboard/src/contexts/UploadManagerContext.tsx (2)
  • useUploadManagerContext (55-63)
  • UploadManagerProvider (20-53)
libs/portal-framework-ui/src/hooks/useApiUrl.ts (1)
  • useApiUrl (3-24)
libs/portal-plugin-dashboard/src/features/upload/Manager.ts (1)
  • DEFAULT_AVATAR_CONFIG (59-64)
libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (1)
  • Dropzone (61-81)
libs/portal-plugin-dashboard/src/contexts/DropzoneContext.tsx (1)
  • useDropzoneContext (233-241)
🪛 Biome (2.1.2)
libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx

[error] 175-175: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 198-198: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🔇 Additional comments (22)
libs/portal-framework-ui/src/components/dialog/types/CustomDialog.tsx (1)

33-37: LGTM: simplified content-only rendering.

Content-only rendering is consistent with the refactor goals.

libs/portal-framework-ui/src/components/dialog/DialogFooterContent.tsx (3)

7-8: LGTM: onCancel threading added.

Propagating onCancel matches the new footer cancellation flow.


15-17: Form dialogs: early return is correct.

Delegating actions to the form when formConfig exists aligns with the new separation of concerns.


20-27: LGTM: switch to centralized renderFooter.

Passing isDialog: true with dialogConfig, footer, and onCancel correctly leverages the unified footer renderer.

libs/portal-framework-ui/src/components/shared/utils/renderFooter.tsx (5)

23-25: Expose onCancel in FooterRenderConfig.

Good addition; enables consistent cancel handling in both standalone and dialog contexts.


50-52: LGTM: onCancel plumbed through the render paths.

Forwarding onCancel into the footer environment covers both custom and default footers.

Also applies to: 66-68, 85-87


113-121: Environment split: clear and safer.

Separating standalone vs dialog environment building reduces ambiguity and null-safety hazards.


147-189: LGTM: dialog env handles onCancel + wizard/step/simple form paths.

Correctly prioritizes wizard with step control, then simple form, else container-only. onClose: onCancel || dialogConfig.onClose is the expected override.


126-142: Confirmed — builder exposes buildContainerOnly()

Found implementation at libs/portal-framework-ui/src/components/shared/environment/builders.ts:186; Environment.footer().standalone().buildContainerOnly() is valid.

libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (4)

110-121: Dialog cancel action generator wiring looks good.

Using createDialogActions with onCancel: environment?.container?.onClose aligns with the new onCancel pathway.


18-20: Unused import removal acknowledged.

Switch to useOptionalFooterContext and dropping useFooterContext is correct.


175-176: Fix conditional hook call (Rules of Hooks).

Call hooks unconditionally; pass optional params instead.

-  config && useEnvironmentSync(environment, config.environmentSync);
+  useEnvironmentSync(environment, config?.environmentSync);

198-199: Fix conditional hook call (Rules of Hooks).

Same issue inside UnifiedFooterInner.

-  config && useEnvironmentSync(environment, config.environmentSync);
+  useEnvironmentSync(environment, config?.environmentSync);
libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (1)

13-21: LGTM: Simpler, explicit footer actions.

Clear shift to explicit actionButtons and horizontal layout looks good.

libs/portal-plugin-dashboard/src/features/upload/Manager.ts (3)

53-57: Good: default configs surfaced for reuse.

Exporting DEFAULT_MAIN_CONFIG and DEFAULT_AVATAR_CONFIG simplifies consumers and ensures consistent defaults.


449-466: CID precedence improvement LGTM.

Prioritizing CID from response over meta is correct and aligns with server-truth.


470-476: Meta fallback for CID LGTM.

Sensible fallback path when response lacks a CID.

libs/portal-plugin-dashboard/src/ui/components/Dropzone.tsx (3)

183-197: Preprocess event wiring LGTM.

Registering preprocess-progress/complete with cleanup ensures PREPROCESSING UI updates.


256-271: Minor JSX refactor LGTM.

Button attribute reformatting doesn’t change behavior; reads cleaner.


283-303: Passing defaultProps to renderDropZone LGTM.

Exposing refs and flags enables custom renderers without poking context.

libs/portal-plugin-dashboard/src/ui/components/AvatarUpload.tsx (2)

59-63: Guard URL parsing when apiUrl is empty/invalid.

Apply:

-    if (!isServiceRegistered) {
-      const apiDomain = new URL(apiUrl).hostname;
-      const apiProto = new URL(apiUrl).protocol;
-      const endpoint = `${apiProto}//account.${apiDomain}/api/account/avatar`;
+    if (!isServiceRegistered) {
+      if (!apiUrl) {
+        console.warn("AvatarUpload: missing apiUrl; skipping service registration");
+        return;
+      }
+      let parsedUrl: URL;
+      try {
+        parsedUrl = new URL(apiUrl);
+      } catch {
+        console.warn(`AvatarUpload: invalid apiUrl "${apiUrl}"`);
+        return;
+      }
+      const apiDomain = parsedUrl.hostname;
+      const apiProto = parsedUrl.protocol;
+      const endpoint = `${apiProto}//account.${apiDomain}/api/account/avatar`;

69-76: Parse XHR response in getResponseData (return object, not string).

Apply:

-            getResponseData() {
-              return JSON.stringify({
-                url: currentAvatar,
-              });
-            },
+            // Parse server response (expects JSON).
+            getResponseData(xhr: XMLHttpRequest) {
+              try {
+                return JSON.parse(xhr.responseText || "{}");
+              } catch {
+                return {};
+              }
+            },

…in footer components

- Introduce `FormFooterProps` interface to explicitly define form-specific footer props
- Update `DefaultFooter`, `FormFooter`, `StepFormFooter`, and `WizardFooter` to use more precise prop types
- Ensure `submitLabel` is always a string with safe defaults in `UnifiedFooter`
- Add `FooterType` import to `UnifiedFooter` for type checking
- Fix typo in `isSimpleFooterEnvironment` type guard
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/portal-framework-ui/src/components/shared/footers/WizardFooter.tsx (1)

27-41: Runtime crash: actionButtons may be undefined.

Calling .map on possibly-undefined actionButtons will throw. Default to an empty array before mapping.

-  const styledActions = actionButtons.map((action) => {
+  const styledActions = (actionButtons ?? []).map((action) => {
     if (action.type === ActionItemType.SUBMIT && action.label === "Back") {
       return {
         ...action,
         variant: "outline",
       };
     }
     if (action.type === ActionItemType.SUBMIT && action.label !== "Back") {
       return {
         ...action,
         variant: "default",
       };
     }
     return action;
   });
🧹 Nitpick comments (3)
libs/portal-framework-ui/src/components/shared/registry/types.ts (1)

20-20: Avoid ComponentType with union props; use a union of ComponentTypes instead.

React.ComponentType<BaseFooterProps | FormFooterProps> is unsound for registration (a component that only accepts FormFooterProps cannot safely substitute one that might receive BaseFooterProps). Prefer a union of component types.

Apply this change:

-export type FooterComponent<T = any> = React.ComponentType<BaseFooterProps<T> | FormFooterProps<T>>;
+export type FooterComponent<T = any> =
+  | React.ComponentType<BaseFooterProps<T>>
+  | React.ComponentType<FormFooterProps<T>>;
libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (2)

289-291: Remove duplicated config.footer handling.

config.footer array check is performed twice; keep one.

-    // Check if the footer config has an actions array
-    if (config && Array.isArray(config.footer)) {
-      baseActions = config.footer;
-    }
...
-    // Check if the footer config has an actions array
-    if (config && Array.isArray(config.footer)) {
-      baseActions = config.footer;
-    }
+    // Check if the footer config has an actions array
+    if (config && Array.isArray(config.footer)) {
+      baseActions = config.footer;
+    }

Also applies to: 323-325


312-319: Normalize loading to boolean.

Ensure loading is strictly boolean to avoid undefined.

-        loading:
-          action.type === ActionItemType.SUBMIT &&
-          environment.form?.isSubmitting,
+        loading:
+          action.type === ActionItemType.SUBMIT &&
+          !!environment.form?.isSubmitting,
-      loading:
-        action.type === ActionItemType.SUBMIT && environment.form?.isSubmitting,
+      loading:
+        action.type === ActionItemType.SUBMIT && !!environment.form?.isSubmitting,

Also applies to: 336-344

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb49076 and ded6d4a.

📒 Files selected for processing (7)
  • libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (13 hunks)
  • libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/shared/footers/FormFooter.tsx (2 hunks)
  • libs/portal-framework-ui/src/components/shared/footers/StepFormFooter.tsx (2 hunks)
  • libs/portal-framework-ui/src/components/shared/footers/WizardFooter.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/shared/registry/types.ts (2 hunks)
  • libs/portal-framework-ui/src/components/shared/types/footer.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
libs/portal-framework-ui/src/components/shared/footers/FormFooter.tsx (1)
libs/portal-framework-ui/src/components/shared/types/footer.ts (1)
  • FormFooterProps (48-57)
libs/portal-framework-ui/src/components/shared/footers/StepFormFooter.tsx (1)
libs/portal-framework-ui/src/components/shared/types/footer.ts (1)
  • FormFooterProps (48-57)
libs/portal-framework-ui/src/components/shared/footers/WizardFooter.tsx (1)
libs/portal-framework-ui/src/components/shared/types/footer.ts (1)
  • BaseFooterProps (21-42)
libs/portal-framework-ui/src/components/shared/registry/types.ts (1)
libs/portal-framework-ui/src/components/shared/types/footer.ts (2)
  • BaseFooterProps (21-42)
  • FormFooterProps (48-57)
libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (1)
libs/portal-framework-ui/src/components/shared/types/footer.ts (1)
  • BaseFooterProps (21-42)
libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (3)
libs/portal-framework-ui/src/components/shared/types/form.ts (3)
  • isWizardForm (140-144)
  • isStepForm (129-133)
  • isSimpleForm (118-122)
libs/portal-framework-ui/src/components/shared/registry/types.ts (1)
  • FooterComponent (20-20)
libs/portal-framework-ui/src/components/shared/types/footer.ts (1)
  • BaseFooterProps (21-42)
libs/portal-framework-ui/src/components/shared/types/footer.ts (1)
libs/portal-framework-ui/src/components/shared/types/form.ts (1)
  • SimpleFormEnvironment (62-75)
🪛 Biome (2.1.2)
libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx

[error] 177-177: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 200-200: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🔇 Additional comments (5)
libs/portal-framework-ui/src/components/shared/footers/StepFormFooter.tsx (1)

6-6: FormFooterProps adoption looks correct.

Props shape aligns with the new FormFooterProps and wiring to createFormActions is consistent.

Also applies to: 16-16

libs/portal-framework-ui/src/components/shared/footers/FormFooter.tsx (1)

6-6: LGTM — prop type update and action wiring are consistent.

No behavioral regressions detected.

Also applies to: 16-16

libs/portal-framework-ui/src/components/shared/UnifiedFooter.tsx (3)

260-262: Good: submitLabel safely defaults to a string.

Ensures a stable string with a sensible fallback.


177-178: Fix conditional hook calls (Rules of Hooks).

Hooks must be called unconditionally. Call useEnvironmentSync every render and pass possibly undefined config safely.

-  config && useEnvironmentSync(environment, config.environmentSync);
+  useEnvironmentSync(environment, config?.environmentSync);
-  config && useEnvironmentSync(environment, config.environmentSync);
+  useEnvironmentSync(environment, config?.environmentSync);

Also applies to: 200-201


118-123: Confirmed — createDialogActions accepts cancelLabel/onCancel/type and using "confirm" is valid.

createDialogActions (libs/portal-framework-ui/src/components/actions/actionHelpers.ts) has type?: 'alert'|'confirm'|'form' and, when onConfirm is not provided (as in your footer), passing type: "confirm" returns only the cancel action.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (1)

5-5: Use type-only imports to avoid runtime baggage.

Switch to import type for types-only usage.

-import { BaseFooterProps, FooterEnvironment } from "../types/footer";
+import type { BaseFooterProps, FooterEnvironment } from "../types/footer";
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ded6d4a and 04a0779.

📒 Files selected for processing (3)
  • libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (1 hunks)
  • libs/portal-framework-ui/src/components/shared/types/footer.ts (4 hunks)
  • libs/portal-framework-ui/src/components/shared/types/form.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • libs/portal-framework-ui/src/components/shared/types/footer.ts
  • libs/portal-framework-ui/src/components/shared/types/form.ts
🧰 Additional context used
🧬 Code graph analysis (1)
libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (1)
libs/portal-framework-ui/src/components/shared/types/footer.ts (1)
  • BaseFooterProps (21-42)
🔇 Additional comments (3)
libs/portal-framework-ui/src/components/shared/footers/DefaultFooter.tsx (3)

13-13: Props shape looks good (environment included).


16-16: Good guard for optional actions.

Passing [] prevents renderer breakage when actionButtons is undefined.


18-18: No change required — ActionListRenderer expects closeDialog.

Verified: ActionListRendererProps defines and uses closeDialog (libs/portal-framework-ui/src/components/actions/ActionListRenderer.tsx), so closeDialog={onClose} is correct.

…handle undefined form context

- Updated type guards in footer.ts to use `FormType` enum instead of string literals
- Added `environment` prop to `DefaultFooter` component
- Ensured `actionButtons` array is properly defaulted to empty array in `DefaultFooter`
- Made form context parameters optional in form type guards to prevent runtime errors
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant