Skip to content

feat(platform): sticky save bars and conditional save buttons in settings#1555

Merged
Israeltheminer merged 3 commits into
mainfrom
feat/settings-forms-sticky-save
Apr 16, 2026
Merged

feat(platform): sticky save bars and conditional save buttons in settings#1555
Israeltheminer merged 3 commits into
mainfrom
feat/settings-forms-sticky-save

Conversation

@Israeltheminer

@Israeltheminer Israeltheminer commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Show save/submit buttons only when forms are dirty, reducing visual clutter across all settings pages (account, branding, organization, governance)
  • Add sticky bottom positioning to save bars in account, branding, and org settings so they remain visible while scrolling long forms
  • Remove redundant pt-2 padding from dialog footers (form-dialog, automation-create)
  • Improve organization settings skeleton to match the new PageSection-based layout with separators
  • Update i18n: friendlier locale label, add detailsTitle, detailsDescription, organizationIdHint keys (en + de)

Test plan

  • Verify save buttons appear only after editing a field in account, branding, org, and governance settings
  • Scroll long forms (branding) and confirm the save bar stays visible at bottom
  • Check dialog footers in form-dialog and automation-create for correct spacing
  • Verify organization settings skeleton renders correctly during loading
  • Confirm i18n strings render in both English and German

Summary by CodeRabbit

Release Notes

  • New Features

    • Action buttons in settings forms now conditionally appear only when unsaved changes exist, with sticky bottom positioning for better accessibility.
  • Bug Fixes & Improvements

    • Refined form layout spacing and padding across dialogs.
    • Enhanced organization settings labels and locale field messaging.
    • Improved loading state visuals in skeleton screens.
    • Simplified form submission button behavior across multiple settings pages.

…ings forms

Show save/submit buttons only when the form is dirty, reducing visual noise.
Add sticky positioning to save bars in account, branding, and org settings
so they remain visible while scrolling. Remove redundant pt-2 padding from
dialog footers. Update org settings i18n: friendlier locale label, add
detailsTitle/detailsDescription/organizationIdHint keys, and improve the
organization page skeleton to match the new layout.
The save button is now only rendered when the form is dirty. Update tests
to verify it's hidden when clean and shown when dirty.
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The PR refactors form and settings components to improve UX patterns. Key changes include: removing top padding utility (pt-2) from dialog footer containers; converting submit buttons from always-rendered (with disabled state) to conditionally rendered only when forms are dirty; applying sticky positioning to action containers at the bottom of screens; and restructuring skeleton loaders for organization settings. Supporting changes update localization strings for organization settings by adding new keys (detailsTitle, detailsDescription, organizationIdHint), changing the defaultLocale label wording, and removing the defaultLocaleDescription key. Corresponding test expectations are updated to verify conditional button rendering.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ 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 main changes: implementing sticky save bars and conditional save buttons across settings pages, which are the primary themes of this changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/settings-forms-sticky-save

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/platform/app/features/settings/account/components/account-form.tsx`:
- Around line 134-137: The submit Button's disabled predicate redundantly checks
!isDirty even though rendering is already gated by isDirty; update the Button
(component instance using props isSubmitting, isDirty, isValid) to remove the
redundant !isDirty check and use a simplified disabled expression (e.g.,
disabled when isSubmitting OR !isValid) so the prop becomes based only on
isSubmitting and isValid.

In
`@services/platform/app/features/settings/branding/components/__tests__/branding-form.test.tsx`:
- Around line 88-99: Add a test in branding-form.test.tsx that after changing
the BrandingForm input (use the same getByLabelText call used in the existing
test for 'branding.appName' and fireEvent.change) you revert the field back to
its original value and then assert the save button (role 'button', name
'actions.saveChanges') is no longer present; use queryByRole and wrap assertions
in waitFor to avoid timing flakes and reference the BrandingForm render and
isDirty behavior so the test verifies the dirty->clean transition.

In
`@services/platform/app/features/settings/organization/components/organization-settings.tsx`:
- Around line 177-183: The submit Button in organization-settings.tsx is
rendered only when isDirty is true, so the disabled prop should drop the
redundant check (!isDirty); update the Button (the JSX block that uses isDirty
and isSubmitting) to set disabled based solely on isSubmitting (e.g.,
disabled={isSubmitting}) to simplify the logic and avoid unreachable conditions.

In `@services/platform/app/routes/dashboard/`$id/settings/organization.tsx:
- Around line 42-49: The loading skeleton includes UI elements that don't appear
in the real OrganizationSettings (extra Separator components and a persistent
save-button placeholder), causing layout shift; update the loading markup in the
same component so it matches OrganizationSettings: remove the unnecessary
Separator elements and replace the persistent save-button Skeleton (the element
with className "h-9 w-28 shrink-0") with either no placeholder or a conditional
skeleton that only renders when the real form shows a save button (i.e., mimic
the same conditional/save-when-dirty behavior used by OrganizationSettings),
locating these changes around the HStack/Stack skeleton nodes in the
organization settings route.

In `@services/platform/messages/de.json`:
- Line 610: The German locale entry for the "defaultLocale" key uses the word
"Agenten" which conflicts with the repo's guideline to keep "Agent/Agents" in
English; update the string value for defaultLocale to replace "Agenten" with
"Agent" so it reads using the English term while preserving the rest of the
German text (locate the "defaultLocale" key in
services/platform/messages/de.json and change only the word "Agenten" to
"Agent").
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d6873fb9-1ae7-4463-8ec3-320e3d7026b7

📥 Commits

Reviewing files that changed from the base of the PR and between 85b90e3 and 363e096.

📒 Files selected for processing (11)
  • services/platform/app/components/ui/dialog/form-dialog.tsx
  • services/platform/app/features/automations/components/automation-create-dialog.tsx
  • services/platform/app/features/settings/account/components/account-form.tsx
  • services/platform/app/features/settings/branding/components/__tests__/branding-form.test.tsx
  • services/platform/app/features/settings/branding/components/branding-form.tsx
  • services/platform/app/features/settings/governance/components/login-policy-editor.tsx
  • services/platform/app/features/settings/governance/components/system-prompt-editor.tsx
  • services/platform/app/features/settings/organization/components/organization-settings.tsx
  • services/platform/app/routes/dashboard/$id/settings/organization.tsx
  • services/platform/messages/de.json
  • services/platform/messages/en.json

Comment on lines +134 to +137
<Button
type="submit"
disabled={isSubmitting || !isDirty || !isValid}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Simplify the submit button disabled predicate.

Since rendering is already gated by isDirty, the !isDirty condition is redundant.

♻️ Proposed simplification
-              disabled={isSubmitting || !isDirty || !isValid}
+              disabled={isSubmitting || !isValid}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button
type="submit"
disabled={isSubmitting || !isDirty || !isValid}
>
<Button
type="submit"
disabled={isSubmitting || !isValid}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/settings/account/components/account-form.tsx`
around lines 134 - 137, The submit Button's disabled predicate redundantly
checks !isDirty even though rendering is already gated by isDirty; update the
Button (component instance using props isSubmitting, isDirty, isValid) to remove
the redundant !isDirty check and use a simplified disabled expression (e.g.,
disabled when isSubmitting OR !isValid) so the prop becomes based only on
isSubmitting and isValid.

Comment on lines +88 to 99
it('shows save button when form is dirty', async () => {
render(<BrandingForm {...defaultProps} />);

const input = screen.getByLabelText('branding.appName', { exact: false });
fireEvent.change(input, { target: { value: 'Acme Corp' } });

const button = screen.getByRole('button', { name: 'actions.saveChanges' });
await waitFor(() => {
expect(button).not.toBeDisabled();
expect(
screen.getByRole('button', { name: 'actions.saveChanges' }),
).toBeInTheDocument();
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add an edge-case test for reverting back to clean state.

After editing, revert the field to its original value and assert the save button disappears again. This protects isDirty regression behavior.

🧪 Suggested test addition
+  it('hides save button again when changes are reverted', async () => {
+    render(<BrandingForm {...defaultProps} />);
+
+    const input = screen.getByLabelText('branding.appName', { exact: false });
+    fireEvent.change(input, { target: { value: 'Acme Corp' } });
+    await screen.findByRole('button', { name: 'actions.saveChanges' });
+
+    fireEvent.change(input, { target: { value: '' } });
+    await waitFor(() => {
+      expect(
+        screen.queryByRole('button', { name: 'actions.saveChanges' }),
+      ).not.toBeInTheDocument();
+    });
+  });
As per coding guidelines: "Tests should cover happy paths, edge cases, and error conditions".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/settings/branding/components/__tests__/branding-form.test.tsx`
around lines 88 - 99, Add a test in branding-form.test.tsx that after changing
the BrandingForm input (use the same getByLabelText call used in the existing
test for 'branding.appName' and fireEvent.change) you revert the field back to
its original value and then assert the save button (role 'button', name
'actions.saveChanges') is no longer present; use queryByRole and wrap assertions
in waitFor to avoid timing flakes and reference the BrandingForm render and
isDirty behavior so the test verifies the dirty->clean transition.

Comment on lines +177 to +183
{isDirty && (
<Button type="submit" disabled={isSubmitting || !isDirty}>
{isSubmitting
? tCommon('actions.saving')
: tCommon('actions.saveChanges')}
</Button>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Remove redundant dirty check in submit button disable state.

!isDirty is unnecessary here because the button only renders when isDirty is true.

♻️ Proposed simplification
-            <Button type="submit" disabled={isSubmitting || !isDirty}>
+            <Button type="submit" disabled={isSubmitting}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/settings/organization/components/organization-settings.tsx`
around lines 177 - 183, The submit Button in organization-settings.tsx is
rendered only when isDirty is true, so the disabled prop should drop the
redundant check (!isDirty); update the Button (the JSX block that uses isDirty
and isSubmitting) to set disabled based solely on isSubmitting (e.g.,
disabled={isSubmitting}) to simplify the logic and avoid unreachable conditions.

Comment on lines +42 to +49
<Separator />
<HStack gap={3} align="end" justify="between">
<Stack gap={1} className="max-w-sm flex-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-9 w-full" />
</Stack>
<Skeleton className="h-9 w-28 shrink-0" />
</HStack>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Skeleton layout does not match the loaded organization settings screen.

Line 42 and Line 72 add separators not present in OrganizationSettings, and Line 48 shows a save-button placeholder even though the loaded form only shows save when dirty. This causes avoidable layout shift after loading.

💡 Suggested fix
       <PageSection
         title={<Skeleton className="h-5 w-40" />}
         description={<Skeleton className="h-4 w-64" />}
         gap={5}
       >
-        <Separator />
         <HStack gap={3} align="end" justify="between">
           <Stack gap={1} className="max-w-sm flex-1">
             <Skeleton className="h-4 w-32" />
             <Skeleton className="h-9 w-full" />
           </Stack>
-          <Skeleton className="h-9 w-28 shrink-0" />
         </HStack>
@@
       <PageSection
         title={<Skeleton className="h-5 w-24" />}
         description={<Skeleton className="h-4 w-52" />}
       >
-        <Separator />
         <HStack justify="between">
           <Skeleton className="h-8 max-w-sm flex-1" />
         </HStack>

Also applies to: 72-72

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/routes/dashboard/`$id/settings/organization.tsx around
lines 42 - 49, The loading skeleton includes UI elements that don't appear in
the real OrganizationSettings (extra Separator components and a persistent
save-button placeholder), causing layout shift; update the loading markup in the
same component so it matches OrganizationSettings: remove the unnecessary
Separator elements and replace the persistent save-button Skeleton (the element
with className "h-9 w-28 shrink-0") with either no placeholder or a conditional
skeleton that only renders when the real form shows a save button (i.e., mimic
the same conditional/save-when-dirty behavior used by OrganizationSettings),
locating these changes around the HStack/Stack skeleton nodes in the
organization settings route.

"manageAccess": "Zugriff auf die Organisation verwalten",
"defaultLocale": "Standardsprache",
"defaultLocaleDescription": "Die Basissprache für Agentennamen, Beschreibungen und Gesprächseinstiege.",
"defaultLocale": "Welche Sprache sollen deine Agenten verwenden?",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use repo-standard “Agents” terminology in German locale.

Line 610 uses Agenten, but this codebase’s de locale standard keeps Agent/Agents in English for consistency.

💡 Suggested fix
-      "defaultLocale": "Welche Sprache sollen deine Agenten verwenden?",
+      "defaultLocale": "Welche Sprache sollen deine Agents verwenden?",

Based on learnings: German (de) terminology guideline says to use “Agent” for Agent and keep it in English.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"defaultLocale": "Welche Sprache sollen deine Agenten verwenden?",
"defaultLocale": "Welche Sprache sollen deine Agents verwenden?",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/messages/de.json` at line 610, The German locale entry for
the "defaultLocale" key uses the word "Agenten" which conflicts with the repo's
guideline to keep "Agent/Agents" in English; update the string value for
defaultLocale to replace "Agenten" with "Agent" so it reads using the English
term while preserving the rest of the German text (locate the "defaultLocale"
key in services/platform/messages/de.json and change only the word "Agenten" to
"Agent").

Since the save buttons are now only rendered when isDirty is true, the
!isDirty condition in the disabled prop is always false and can be removed.
@Israeltheminer Israeltheminer merged commit 08d21dd into main Apr 16, 2026
25 checks passed
@Israeltheminer Israeltheminer deleted the feat/settings-forms-sticky-save branch April 16, 2026 17:31
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