Skip to content

refactor(platform): convert provider dialogs to sidepanels#1410

Merged
Israeltheminer merged 2 commits into
mainfrom
refactor/provider-sidepanel
Apr 11, 2026
Merged

refactor(platform): convert provider dialogs to sidepanels#1410
Israeltheminer merged 2 commits into
mainfrom
refactor/provider-sidepanel

Conversation

@Israeltheminer
Copy link
Copy Markdown
Collaborator

@Israeltheminer Israeltheminer commented Apr 11, 2026

Summary

  • Replace FormDialog-based provider add/edit modals with Sheet-based sidepanels matching the integration panel pattern (header bar, scrollable content, sticky footer)
  • Extract inline setup guide markup from integration-credentials-form into a reusable CollapsibleGuide component with defaultOpen prop
  • Add real-time validation to the provider edit panel so submit stays disabled until fields are valid
  • Clarify read-only provider name field with descriptive helper text
  • Remove duplicate ### Configuration Guide heading from AI Image integration's setupGuide content

Closes #1387 & #1382

Test plan

  • Open add provider panel — verify it slides in from the right as a sidepanel
  • Verify the "Getting started" guide is expanded by default in add provider
  • Verify submit button stays disabled until all required fields are filled
  • Open edit provider panel — verify sidepanel layout matches add panel
  • Verify provider name field is read-only with explanatory helper text
  • Verify edit submit button is disabled when fields are empty or unchanged
  • Open an integration with a setup guide — verify CollapsibleGuide renders correctly
  • Verify AI Image integration setup guide no longer shows duplicate heading

Summary by CodeRabbit

  • New Features

    • Added collapsible guide component for expandable setup instructions
    • Converted provider management from dialog-based to panel-based UI for improved usability
  • Improvements

    • Provider names now display as read-only with clarifying help text
  • Localization

    • Added German and English translations for provider configuration guidance and help text

Replace FormDialog-based provider add/edit modals with Sheet-based
sidepanels matching the integration panel pattern. Extract inline
setup guide markup into a reusable CollapsibleGuide component.
Add edit-form validation and clarify read-only provider name field.
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 11, 2026

📝 Walkthrough

Walkthrough

This PR refactors the provider management UI by replacing dialog-based components (ProviderAddDialog, ProviderEditDialog) with new sheet-based panel components (ProviderAddPanel, ProviderEditPanel). Additionally, a new reusable CollapsibleGuide component is introduced to standardize the rendering of collapsible markdown content, which replaces inline details implementations in the integration credentials form. Supporting changes include reformatting the ai-image configuration guide text and adding localization strings for provider name read-only help text and BYOM guidance section titles.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • tale-project/tale#1319: Directly replaces dialog-based provider components with panel-based alternatives and updates related i18n keys
  • tale-project/tale#1321: Modifies the same providers table file to adjust component integrations and toast notifications
  • tale-project/tale#1409: Updates the integration credentials form with the new CollapsibleGuide component and modifies provider-related form labels
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive PR adds descriptive helper text to clarify read-only provider name behavior in edit panel, addressing issue #1387's concern about unclear field state without making it editable as originally requested. Clarify whether the helper text adequately addresses issue #1387 or if the provider name field should actually be made editable instead of remaining read-only.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately reflects the main change: converting provider dialogs to sidepanels, which is the central refactoring effort across multiple components.
Out of Scope Changes check ✅ Passed All changes are in scope: CollapsibleGuide extraction, provider panel/dialog refactoring, localization strings, and setupGuide formatting are all aligned with PR objectives and issue #1387.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 refactor/provider-sidepanel

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

🤖 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/components/ui/data-display/collapsible-guide.tsx`:
- Around line 22-25: The <details> is currently controlled via
open={defaultOpen}, which forces its state on every parent render; remove the
controlled prop and instead set the initial open state only on mount (e.g., use
a ref for the <details> element and in useEffect set detailsRef.current.open =
defaultOpen once). Update the component in collapsible-guide.tsx to stop passing
open={defaultOpen} and implement the useRef/useEffect initialization so user
toggles remain uncontrolled after mount.

In
`@services/platform/app/features/settings/providers/components/provider-add-panel.tsx`:
- Around line 289-293: The error message Text elements (e.g., the one rendering
errors.models?.root?.message using <Text variant="caption"
className="text-destructive text-sm">) need an accessibility attribute; add
role="alert" to these Text components so screen readers announce the error
immediately; apply the same change to the other error display (the Text
rendering the model-level error at the second location around lines 358-365) to
ensure both error messages include role="alert".
- Line 45: The component is accepting an organizationId prop (_organizationId)
but the calls to saveProvider and saveProviderSecret use the hardcoded string
'default'; either use the passed prop or remove it from the props/interface. Fix
option A: replace the hardcoded 'default' in the saveProvider and
saveProviderSecret calls with the received _organizationId (or a safe fallback
like _organizationId ?? 'default') so ProviderAddPanel actually uses the prop.
Fix option B: if multi-org is not needed, remove organizationId/_organizationId
from the component props/interface and any related tests/usage so it isn't
unused. Update references around the saveProvider and saveProviderSecret
invocations accordingly.
- Around line 70-74: The baseUrl zod schema in provider-add-panel.tsx currently
passes the "required" translation to .url(), causing invalid-URL errors to show
as "required"; change the chain to enforce an explicit empty-string/required
message and a separate URL-format message: call
.nonempty(tCommon('validation.required', { field: t('providers.baseUrl') })) (or
.min(1, ...)) and then .url(tCommon('validation.url', { field:
t('providers.baseUrl') })) on the baseUrl field so empty and malformed URLs
produce the correct localized messages.
- Around line 162-195: The current onSubmit flow calls saveProvider then
saveProviderSecret and treats any error the same, causing a silent partial
success if the secret save fails; update onSubmit to detect saveProviderSecret
failures and handle them explicitly: after calling saveProvider and if
saveProviderSecret throws, either call a cleanup API (e.g. deleteProvider with
the same orgSlug and providerName) to roll back the created provider or surface
a distinct user message that the provider was created but the API key was not
saved and they must add it via the edit flow; implement this by wrapping
saveProviderSecret in its own try/catch inside onSubmit and use
deleteProvider(providerName) or a descriptive toast (instead of the generic
createFailed toast), while keeping reset() and onOpenChange(false) only on full
success.

In
`@services/platform/app/features/settings/providers/components/provider-edit-panel.tsx`:
- Around line 73-74: The current isValid computation only checks non-empty
strings (const isValid) so invalid strings like "not-a-url" enable the save
button; update the client-side validation to verify form.baseUrl is a
well-formed URL (e.g., attempt new URL(form.baseUrl) or use a lightweight regex)
and incorporate that into isValid. Also surface a user-facing validation error
for baseUrl in the provider edit form (the same place that reads form.baseUrl
and sets form.displayName) so the Save button stays disabled when the URL parse
fails and the user sees feedback before submission. Ensure references to isValid
and form.baseUrl (in provider-edit-panel.tsx) are updated accordingly.
🪄 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: 733c8189-359c-48b5-9eea-1f5b0cbe3c6e

📥 Commits

Reviewing files that changed from the base of the PR and between 88e540d and 0b008cf.

📒 Files selected for processing (10)
  • examples/integrations/ai-image/config.json
  • services/platform/app/components/ui/data-display/collapsible-guide.tsx
  • services/platform/app/features/settings/integrations/components/integration-manage/integration-credentials-form.tsx
  • services/platform/app/features/settings/providers/components/provider-add-dialog.tsx
  • services/platform/app/features/settings/providers/components/provider-add-panel.tsx
  • services/platform/app/features/settings/providers/components/provider-edit-dialog.tsx
  • services/platform/app/features/settings/providers/components/provider-edit-panel.tsx
  • services/platform/app/features/settings/providers/components/providers-table.tsx
  • services/platform/messages/de.json
  • services/platform/messages/en.json
💤 Files with no reviewable changes (2)
  • services/platform/app/features/settings/providers/components/provider-add-dialog.tsx
  • services/platform/app/features/settings/providers/components/provider-edit-dialog.tsx

Comment on lines +22 to +25
<details
className="bg-muted/30 border-border rounded-lg border"
open={defaultOpen}
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n services/platform/app/components/ui/data-display/collapsible-guide.tsx

Repository: tale-project/tale

Length of output: 1407


🏁 Script executed:

rg -l "CollapsibleGuide" --type tsx --type ts

Repository: tale-project/tale

Length of output: 88


🏁 Script executed:

rg "CollapsibleGuide" --type ts --type js

Repository: tale-project/tale

Length of output: 1000


🏁 Script executed:

rg -A 5 "CollapsibleGuide" services/platform/app/features/settings/providers/components/provider-add-panel.tsx | head -20

Repository: tale-project/tale

Length of output: 640


🏁 Script executed:

rg -A 5 "CollapsibleGuide" services/platform/app/features/settings/integrations/components/integration-manage/integration-credentials-form.tsx | head -20

Repository: tale-project/tale

Length of output: 606


defaultOpen is wired as a controlled open prop, causing user-toggled state to reset on parent rerender.

When a parent component rerenders, React will update the <details> element's open attribute to the defaultOpen value, overriding any user interactions (like closing the disclosure). This breaks the expected "default-only" behavior.

Proposed fix
 import { Info } from 'lucide-react';
+import { useState } from 'react';
 import ReactMarkdown from 'react-markdown';
 import remarkGfm from 'remark-gfm';
@@
 export function CollapsibleGuide({
   label,
   content,
   defaultOpen,
 }: CollapsibleGuideProps) {
+  const [isOpen, setIsOpen] = useState(Boolean(defaultOpen));
+
   return (
     <details
       className="bg-muted/30 border-border rounded-lg border"
-      open={defaultOpen}
+      open={isOpen}
+      onToggle={(event) => setIsOpen(event.currentTarget.open)}
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/components/ui/data-display/collapsible-guide.tsx`
around lines 22 - 25, The <details> is currently controlled via
open={defaultOpen}, which forces its state on every parent render; remove the
controlled prop and instead set the initial open state only on mount (e.g., use
a ref for the <details> element and in useEffect set detailsRef.current.open =
defaultOpen once). Update the component in collapsible-guide.tsx to stop passing
open={defaultOpen} and implement the useRef/useEffect initialization so user
toggles remain uncontrolled after mount.

export function ProviderAddPanel({
open,
onOpenChange,
organizationId: _organizationId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Unused organizationId prop with hardcoded fallback.

The organizationId prop is received but not used—'default' is hardcoded in both saveProvider and saveProviderSecret calls (lines 165 and 178). If multi-org support is planned, consider using the prop; otherwise, remove it from the interface to avoid confusion.

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

In
`@services/platform/app/features/settings/providers/components/provider-add-panel.tsx`
at line 45, The component is accepting an organizationId prop (_organizationId)
but the calls to saveProvider and saveProviderSecret use the hardcoded string
'default'; either use the passed prop or remove it from the props/interface. Fix
option A: replace the hardcoded 'default' in the saveProvider and
saveProviderSecret calls with the received _organizationId (or a safe fallback
like _organizationId ?? 'default') so ProviderAddPanel actually uses the prop.
Fix option B: if multi-org is not needed, remove organizationId/_organizationId
from the component props/interface and any related tests/usage so it isn't
unused. Update references around the saveProvider and saveProviderSecret
invocations accordingly.

Comment on lines +70 to +74
baseUrl: z.string().url(
tCommon('validation.required', {
field: t('providers.baseUrl'),
}),
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

URL validation uses incorrect error message.

The baseUrl field uses .url() validation but displays a "required" error message. When users enter an invalid URL format, they'll see "Base URL is required" instead of a URL format error, which is misleading.

🔧 Proposed fix
         baseUrl: z.string().url(
-          tCommon('validation.required', {
-            field: t('providers.baseUrl'),
-          }),
+          t('providers.invalidBaseUrl'),
         ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/settings/providers/components/provider-add-panel.tsx`
around lines 70 - 74, The baseUrl zod schema in provider-add-panel.tsx currently
passes the "required" translation to .url(), causing invalid-URL errors to show
as "required"; change the chain to enforce an explicit empty-string/required
message and a separate URL-format message: call
.nonempty(tCommon('validation.required', { field: t('providers.baseUrl') })) (or
.min(1, ...)) and then .url(tCommon('validation.url', { field:
t('providers.baseUrl') })) on the baseUrl field so empty and malformed URLs
produce the correct localized messages.

Comment on lines +162 to +195
const onSubmit = async (data: FormData) => {
try {
await saveProvider({
orgSlug: 'default',
providerName: data.name,
config: {
displayName: data.displayName,
baseUrl: data.baseUrl,
models: data.models.map((m) => ({
id: m.id,
displayName: m.displayName,
tags: m.tags,
})),
},
});
await saveProviderSecret({
orgSlug: 'default',
providerName: data.name,
apiKey: data.apiKey,
});
toast({
title: t('providers.created'),
variant: 'success',
});
reset();
onOpenChange(false);
} catch (error) {
console.error(error);
toast({
title: t('providers.createFailed'),
variant: 'destructive',
});
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Partial failure risk when saving provider secret.

If saveProviderSecret fails after saveProvider succeeds (lines 164-181), the provider exists without an API key. The user sees the "createFailed" toast but the provider was actually created. Consider either:

  1. Informing the user that the provider was created but the key wasn't saved, so they can add it via the edit flow
  2. Attempting to clean up the provider on secret save failure (if a delete API exists)
💡 Suggested improvement for clearer error messaging
     } catch (error) {
       console.error(error);
       toast({
-        title: t('providers.createFailed'),
+        title: t('providers.secretSaveFailed'),
+        description: t('providers.providerCreatedButKeyFailed'), // add this key
         variant: 'destructive',
       });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/settings/providers/components/provider-add-panel.tsx`
around lines 162 - 195, The current onSubmit flow calls saveProvider then
saveProviderSecret and treats any error the same, causing a silent partial
success if the secret save fails; update onSubmit to detect saveProviderSecret
failures and handle them explicitly: after calling saveProvider and if
saveProviderSecret throws, either call a cleanup API (e.g. deleteProvider with
the same orgSlug and providerName) to roll back the created provider or surface
a distinct user message that the provider was created but the API key was not
saved and they must add it via the edit flow; implement this by wrapping
saveProviderSecret in its own try/catch inside onSubmit and use
deleteProvider(providerName) or a descriptive toast (instead of the generic
createFailed toast), while keeping reset() and onOpenChange(false) only on full
success.

Comment on lines +289 to +293
{errors.models?.root?.message && (
<Text variant="caption" className="text-destructive text-sm">
{errors.models.root.message}
</Text>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add role="alert" to error messages for screen reader announcements.

Per accessibility guidelines, error messages should use role="alert" so screen readers announce them immediately when they appear. This applies here and to the similar error display at lines 358-365.

♿ Proposed fix
             {errors.models?.root?.message && (
-               <Text variant="caption" className="text-destructive text-sm">
+               <Text variant="caption" className="text-destructive text-sm" role="alert">
                   {errors.models.root.message}
                 </Text>
             )}

As per coding guidelines: "USE role='alert' on error messages."

📝 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
{errors.models?.root?.message && (
<Text variant="caption" className="text-destructive text-sm">
{errors.models.root.message}
</Text>
)}
{errors.models?.root?.message && (
<Text variant="caption" className="text-destructive text-sm" role="alert">
{errors.models.root.message}
</Text>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/settings/providers/components/provider-add-panel.tsx`
around lines 289 - 293, The error message Text elements (e.g., the one rendering
errors.models?.root?.message using <Text variant="caption"
className="text-destructive text-sm">) need an accessibility attribute; add
role="alert" to these Text components so screen readers announce the error
immediately; apply the same change to the other error display (the Text
rendering the model-level error at the second location around lines 358-365) to
ensure both error messages include role="alert".

Comment on lines +73 to +74
const isValid =
form.displayName.trim().length > 0 && form.baseUrl.trim().length > 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider validating URL format client-side.

The isValid check only verifies non-empty values, but baseUrl must be a valid URL (per the server-side schema). Users can enter invalid URLs like "not-a-url", the save button enables, but submission fails server-side. Adding a simple URL format check would provide earlier feedback.

💡 Suggested enhancement
+ const isValidUrl = (url: string) => {
+   try {
+     new URL(url);
+     return true;
+   } catch {
+     return false;
+   }
+ };
+
  const isValid =
-   form.displayName.trim().length > 0 && form.baseUrl.trim().length > 0;
+   form.displayName.trim().length > 0 &&
+   form.baseUrl.trim().length > 0 &&
+   isValidUrl(form.baseUrl.trim());
📝 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
const isValid =
form.displayName.trim().length > 0 && form.baseUrl.trim().length > 0;
const isValidUrl = (url: string) => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
const isValid =
form.displayName.trim().length > 0 &&
form.baseUrl.trim().length > 0 &&
isValidUrl(form.baseUrl.trim());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/settings/providers/components/provider-edit-panel.tsx`
around lines 73 - 74, The current isValid computation only checks non-empty
strings (const isValid) so invalid strings like "not-a-url" enable the save
button; update the client-side validation to verify form.baseUrl is a
well-formed URL (e.g., attempt new URL(form.baseUrl) or use a lightweight regex)
and incorporate that into isValid. Also surface a user-facing validation error
for baseUrl in the provider edit form (the same place that reads form.baseUrl
and sets form.displayName) so the Save button stays disabled when the URL parse
fails and the user sees feedback before submission. Ensure references to isValid
and form.baseUrl (in provider-edit-panel.tsx) are updated accordingly.

Use controlled state for CollapsibleGuide to preserve user toggle
across rerenders. Add role="alert" to form error messages for
screen reader announcements. Fix base URL validation to show proper
URL format error instead of "required". Add URL format validation
to edit panel.
@Israeltheminer Israeltheminer merged commit fd44fd5 into main Apr 11, 2026
23 of 24 checks passed
@Israeltheminer Israeltheminer deleted the refactor/provider-sidepanel branch April 11, 2026 09:20
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.

Provider name is not editable in “Edit provider” flow

1 participant