Skip to content

[Feat] Dialog Refactor#1140

Open
RichardAnderson wants to merge 8 commits into
vitodeploy:4.xfrom
RichardAnderson:feat/dialog-rendering
Open

[Feat] Dialog Refactor#1140
RichardAnderson wants to merge 8 commits into
vitodeploy:4.xfrom
RichardAnderson:feat/dialog-rendering

Conversation

@RichardAnderson
Copy link
Copy Markdown
Member

@RichardAnderson RichardAnderson commented May 31, 2026

Refactors dialogs to ensure they render outside of the dropdown, resolves problems such as live updates changing tables and rebinding data, updating the wrong row, or streaming the wrong log file.

Summary by CodeRabbit

  • New Features

    • Centralized dialog system added (host, typed registry, hook/store) and a deployment history table with realtime, sortable columns.
  • Improvements

    • Replaced many inline modals with the shared dialog API; standardized controlled dialog behavior, confirm flows, focus restoration, and accessibility/focus cleanup.
  • Documentation

    • Frontend guidelines updated with comprehensive dialog patterns and rules.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@RichardAnderson RichardAnderson marked this pull request as ready for review May 31, 2026 17:23
@saeedvaziry saeedvaziry requested a review from Copilot May 31, 2026 17:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a27125cd-ebc2-42a0-afe8-03a92f840809

📥 Commits

Reviewing files that changed from the base of the PR and between f381aa3 and 05b38ea.

📒 Files selected for processing (5)
  • resources/js/components/dialogs/php-extensions-dialog.tsx
  • resources/js/components/dialogs/plugin-logs-dialog.tsx
  • resources/js/pages/servers/components/actions.tsx
  • resources/js/pages/site-settings/components/vhost.tsx
  • resources/js/stores/dialog-store.ts
✅ Files skipped from review due to trivial changes (1)
  • resources/js/stores/dialog-store.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • resources/js/components/dialogs/php-extensions-dialog.tsx
  • resources/js/pages/servers/components/actions.tsx

📝 Walkthrough

Walkthrough

Implements a typed dialog registry, a Zustand dialog store, a DialogHost and useDialog hook; adds multiple registered dialog components, integrates the host into the app layout, converts many inline dialogs/actions to registry-driven open calls, and introduces DeploymentTable with controller wiring and small UI/table tweaks.

Changes

Centralized dialog system and deployments table

Layer / File(s) Summary
Dialog infra, registry, store, hook, and types
resources/js/components/dialogs/registry.ts, resources/js/stores/dialog-store.ts, resources/js/hooks/use-dialog.ts
Adds typed dialogs registry, DialogControlProps/ConsumerProps types, a Zustand useDialogStore managing a single active dialog (open/close and focus restore), and a memoized useDialog() hook exposing typed open(props) / close() accessors.
Dialog host and layout integration
resources/js/components/dialogs/dialog-host.tsx, resources/js/layouts/app/layout.tsx
Adds DialogHost component that renders the active registered dialog from the store and wires Inertia navigation to close dialogs; integrates DialogHost into the main app layout.
Confirmation and viewer dialogs
resources/js/components/dialogs/confirmation-dialog.tsx, resources/js/components/dialogs/log-viewer-dialog.tsx, resources/js/components/dialogs/worker-logs-dialog.tsx
Adds a generic ConfirmationDialog (typed POST/DELETE/PATCH/PUT submission via Inertia/useForm), a LogViewerDialog for server logs with download link, and a WorkerLogsDialog that polls worker logs when open.
Editor and service dialogs
resources/js/components/dialogs/php-ini-dialog.tsx, resources/js/pages/services/components/config-file-dialog.tsx, resources/js/components/dialogs/php-extensions-dialog.tsx
Adds sheet-based editors for PHP INI and service config files (react-query + Monaco editor integrations) and a PHP extensions installer dialog, each closing on successful submit.
Dialog-driven UI refactors (mass changes)
resources/js/pages/**/*, resources/js/components/server-banners.tsx, resources/js/pages/...
Replaces many inline/local dialog implementations with menu items or buttons that call useDialog().<key>.open(...) (including confirm flows via dialog.confirm.open(...)) across backups, commands, cronjobs, databases, domains, firewall, hosted-domains, monitoring, notification channels, plugins, projects, scripts, services, source-controls, servers, workers, and more; many form/dialog components were converted to controlled components or moved to the registry.
New/updated dialog components
resources/js/components/dialogs/*, resources/js/pages/*/components/*-dialog.tsx
Adds multiple dialog components (ActivateServerSslDialog, DataRetentionDialog, NotificationChannelEditDialog, ServerProviderEditDialog, StorageProviderEditDialog, LinkDatabaseUserDialog, SourceControlEditDialog, ServiceConfigFileDialog, PhpIniDialog, PhpExtensionsDialog, PluginLogsDialog, etc.) used by the registry.
Deployment table and controller changes
app/Tables/DeploymentTable.php, app/Http/Controllers/ApplicationController.php, app/Enums/DeploymentStatus.php
Adds DeploymentTable class for InertiaTable rendering and updates ApplicationController@index to use DeploymentTable::make(...)->paginate(); DeploymentStatus enum now implements HasTableDisplay.
Minor UI/table tweaks
app/Tables/Servers/FirewallRuleTable.php, resources/js/pages/sites/stats.tsx
Removes uppercase transform from firewall rule type/protocol columns and fixes duplicate-key React lists by including index in keys.
Docs
.github/instructions/frontend.instructions.md, CLAUDE.md
Adds a “Dialogs (centralized registry)” section and a CLAUDE rule requiring dialogs use the centralized registry and forbidding DialogTrigger nesting within DropdownMenuItem.

Sequence Diagram(s)

sequenceDiagram
  participant UI as UI Component
  participant Hook as useDialog()
  participant Store as useDialogStore
  participant Host as DialogHost
  participant Dialog as RegisteredDialog

  UI->>Hook: dialog.<key>.open(typedProps)
  Hook->>Store: open(key, props)
  Store-->>Host: active={key, props}
  Host->>Dialog: render(open=true, props)
  Dialog-->>Store: onOpenChange(false) -> close()
  Store-->>Host: clear active, restore focus
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A rabbit taps the dialog drum,
One hook to call — the modals come.
Host nests them, focus finds its track,
Confirm, edit, logs — handlers back.
Tables tidy, deployments hum. 🐇✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 17

Caution

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

⚠️ Outside diff range comments (7)
resources/js/pages/backups/components/edit-backup.tsx (1)

35-95: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use form-native submit for “Save” in the edit backup dialog

  • The form already calls onSubmit={submit} (which does e.preventDefault()), but the “Save” button bypasses it by using type="button" with onClick={submit}.
Suggested fix
-            <Button form="edit-backup-form" type="button" onClick={submit} disabled={form.processing}>
+            <Button form="edit-backup-form" type="submit" disabled={form.processing}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/pages/backups/components/edit-backup.tsx` around lines 35 - 95,
The Save button is bypassing the form-native submit by using type="button" with
onClick={submit}; change the Button (the one rendering Save that references
form="edit-backup-form") to use type="submit" and remove its onClick handler so
the Form's onSubmit={submit} (and its e.preventDefault() logic) is used
natively; ensure the Form id "edit-backup-form" and the submit function remain
unchanged so validation/processing (form.processing) still controls the disabled
state.
resources/js/pages/cronjobs/components/columns.tsx (1)

28-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix keyboard activation for Cron job dropdown action

DropdownMenuItem currently calls onSelect={(e) => e.preventDefault()} and triggers submit via onClick, so keyboard selection won’t run submit. Put the action on onSelect.

♿ Suggested fix
-    <DropdownMenuItem onSelect={(e) => e.preventDefault()} onClick={submit} disabled={form.processing}>
+    <DropdownMenuItem onSelect={submit} disabled={form.processing}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/pages/cronjobs/components/columns.tsx` around lines 28 - 33, The
DropdownMenuItem currently prevents default in onSelect and triggers submit on
onClick which breaks keyboard activation; move the submit invocation into the
onSelect handler (e.g. call submit inside onSelect), remove or adjust the
existing onClick usage, and ensure you still call e.preventDefault() if needed
before calling submit so the form.post(route(routeName, routeParams)) in submit
runs for both mouse and keyboard while respecting form.processing and showing
LoaderCircleIcon.
resources/js/pages/site-features/components/feature-action.tsx (1)

28-43: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear dynamic form state between action opens.

This controlled dialog now closes on success but no longer resets form data. Values can persist into the next action open and submit unintended fields.

Proposed fix
   const submit = (e: FormEvent) => {
     e.preventDefault();
     form.post(
...
       {
-        onSuccess: () => onOpenChange(false),
+        onSuccess: () => {
+          onOpenChange(false);
+          form.reset();
+          form.clearErrors();
+        },
       },
     );
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/pages/site-features/components/feature-action.tsx` around lines
28 - 43, The form state persists between dialog opens because the useForm
instance (form) is never reset; update the submit flow and dialog open handling
in feature-action.tsx so the form is cleared between uses: call form.reset()
after a successful post (inside submit's onSuccess before/after calling
onOpenChange(false)) and also clear form when the dialog is opened (add an
effect or handler that calls form.reset() when the dialog's open state becomes
true) to ensure no stale values remain; reference the useForm hook, the submit
function, form.post, and onOpenChange to locate where to add the reset calls.
resources/js/pages/scripts/components/form.tsx (1)

16-26: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rehydrate ScriptForm state when reopening or switching scripts

useForm is initialized from script?.name/content only on mount, and the only existing useEffect updates focus—so reopening or switching the script prop can retain stale field values/errors. The Monaco Editor also uses defaultValue, so content won’t reliably update after calling form.setData.

Proposed fix
  useEffect(() => {
    setFocused(open);
    return () => setFocused(false);
  }, [open, setFocused]);
+
+  useEffect(() => {
+    if (!open) return;
+    form.setData({
+      name: script?.name ?? '',
+      content: script?.content ?? '',
+    });
+    form.clearErrors();
+  }, [open, script?.id]);

Also resync Monaco content by switching the Editor to a controlled value={form.data.content} (or remounting it with a key tied to script?.id / reopen).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/pages/scripts/components/form.tsx` around lines 16 - 26,
ScriptForm's useForm is only initialized on mount so reopening or switching
script leaves stale fields and errors; add a useEffect in the ScriptForm
component that watches [script, open] and calls form.setData({ name:
script?.name ?? '', content: script?.content ?? '' }) (and clear errors/state if
needed) to rehydrate the form when the modal is opened or the script prop
changes, and make the Monaco Editor controlled by switching from defaultValue to
value={form.data.content} (or remount it by giving it a key tied to script?.id)
so the editor content stays in sync with form.data.content.
resources/js/pages/server-features/components/feature-action.tsx (1)

68-68: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove click-bound submit to avoid duplicate requests.

This button already targets the form; keeping onClick={submit} can execute submit logic twice. Use explicit type="submit" and let the form onSubmit handle it.

Suggested fix
-          <Button form="action-form" disabled={form.processing} onClick={submit}>
+          <Button form="action-form" type="submit" disabled={form.processing}>

As per coding guidelines, form submit buttons in dialogs must use form="<form-id>" type="submit" and not rely on button onClick.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/pages/server-features/components/feature-action.tsx` at line 68,
The Button currently binds submit via onClick which can trigger duplicate
submissions; remove the onClick={submit} from the Button that references
form="action-form" and instead set type="submit" so the form's onSubmit handles
submission; ensure the existing submit handler (submit) is wired to the form's
onSubmit (not the Button) and keep disabled={form.processing} intact to prevent
double requests.
resources/js/pages/hosted-domains/components/edit-hosted-domain.tsx (1)

44-49: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Edited domain state may leak between dialog sessions.

useForm is seeded from hostedDomain once, and success only closes. If this dialog is reused, stale values can appear for a different domain.

Suggested fix
-import { FormEvent } from 'react';
+import { FormEvent, useEffect } from 'react';
...
+  useEffect(() => {
+    if (!open) return;
+    form.setData({
+      domain: hostedDomain.domain,
+      type: hostedDomain.type,
+      ssl_method: hostedDomain.ssl_method,
+      ssl_id: hostedDomain.ssl_id ? String(hostedDomain.ssl_id) : '',
+    });
+    form.clearErrors();
+  }, [open, hostedDomain.id]);

Also applies to: 73-74

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/pages/hosted-domains/components/edit-hosted-domain.tsx` around
lines 44 - 49, The form created with useForm<EditForm> is only seeded once from
hostedDomain so if the dialog is reused for another hostedDomain it can show
stale values; update the component to reset or reinitialize the form whenever
hostedDomain changes (e.g., in a useEffect that calls the form reset method or
re-creates form state) so fields (domain, type, ssl_method, ssl_id) are updated
for the new hostedDomain; apply the same approach for the other instance
referenced around lines 73-74 to ensure no state leaks between dialog sessions.
resources/js/pages/firewall/components/form.tsx (1)

43-54: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Firewall form can submit stale values after switching rows.

The form is initialized once from firewallRule, and success now only closes the dialog. In a reused controlled dialog, opening another rule can carry over prior state.

Suggested fix
+import { FormEvent, useEffect } from 'react';
...
+  useEffect(() => {
+    if (!open) return;
+    form.setData({
+      name: firewallRule?.name || '',
+      type: firewallRule?.type || '',
+      protocol: firewallRule?.protocol || '',
+      port: firewallRule?.port?.toString() || '',
+      source_any: !firewallRule?.source,
+      source: firewallRule?.source || '',
+      mask: firewallRule?.mask?.toString() || '',
+    });
+    form.clearErrors();
+  }, [open, firewallRule?.id]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/pages/firewall/components/form.tsx` around lines 43 - 54, The
submit handler currently reuses the form instance initialized once from
firewallRule which allows stale values when the dialog is reused; update the
component to reset or reinitialize the form whenever the active firewallRule or
the dialog open state changes and also after a successful submit. Concretely,
add logic (e.g. useEffect or the dialog open handler) that calls form.reset(...)
or form.setData(...) with the current firewallRule fields (or empty defaults
when creating) when firewallRule changes or when opening the dialog, and ensure
the onSuccess callbacks for both form.put and form.post also reset the form (and
then call onOpenChange(false)) so the next open starts with fresh data;
reference the submit function, form.put, form.post, firewallRule and
onOpenChange to locate where to hook the reset.
🧹 Nitpick comments (5)
resources/js/components/dialogs/log-viewer-dialog.tsx (1)

32-34: ⚡ Quick win

Replace raw internal anchor with Inertia Link.

This is an internal route and should use Link/router APIs rather than a raw <a>.

Proposed fix
+import { Link } from '`@inertiajs/react`';
@@
-          <a href={route('logs.download', { server: serverId, log: logId })} target="_blank" rel="noopener noreferrer">
-            <Button variant="outline">Download</Button>
-          </a>
+          <Button asChild variant="outline">
+            <Link href={route('logs.download', { server: serverId, log: logId })} target="_blank" rel="noopener noreferrer">
+              Download
+            </Link>
+          </Button>

As per coding guidelines: "**/*.{ts,tsx}: Use <Link> or router.visit() for internal navigation — never raw <a> tags for internal routes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/components/dialogs/log-viewer-dialog.tsx` around lines 32 - 34,
Replace the raw anchor used for internal navigation with Inertia's Link: change
the <a href={route('logs.download', { server: serverId, log: logId })} ...>
wrapper around the Button to a Link from Inertia (e.g. import { Link } from
'`@inertiajs/react`' or '`@inertiajs/inertia-react`' depending on the project),
keeping the same href (route('logs.download', { server: serverId, log: logId }))
and attributes (target="_blank" rel="noopener noreferrer") and ensure the Link
wraps the Button component so the internal route uses the router API rather than
a raw <a>.
resources/js/pages/domains/components/columns.tsx (1)

7-7: 💤 Low value

Unused import after refactor.

The router import from @inertiajs/react appears to be unused after the dialog refactor removed the inline form submission logic.

🧹 Proposed fix
-import { router } from '`@inertiajs/react`';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/pages/domains/components/columns.tsx` at line 7, Remove the
now-unused router import from `@inertiajs/react` in
resources/js/pages/domains/components/columns.tsx: locate the import line that
includes "router" and delete "router" (or remove the entire import if nothing
else is imported) so there are no unused imports after the dialog refactor.
resources/js/pages/php/components/default-cli.tsx (1)

12-19: ⚡ Quick win

Include an explicit confirm variant in this dialog payload.

This keeps confirm dialog behavior consistent with the project’s standardized confirm contract.

Suggested patch
         dialog.confirm.open({
           title: 'Make default cli',
           description: `Are you sure you want to make PHP ${service.version} the default cli?`,
+          variant: 'default',
           confirmLabel: 'Save',
           method: 'post',
           url: route('php.default-cli', { server: service.server_id, service: service.id }),
           data: { version: service.version },
         })

As per coding guidelines For simple confirm or destructive actions, use dialog.confirm.open() with title, description, variant, confirmLabel, method, and url.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/pages/php/components/default-cli.tsx` around lines 12 - 19, Add
the missing variant key to the dialog.confirm.open payload to comply with the
confirm contract: update the call to dialog.confirm.open (in default-cli.tsx) to
include a variant property (e.g., variant: 'confirm' or 'destructive' as
appropriate for this action) alongside title, description, confirmLabel, method,
url and data so the dialog behavior matches the project's standardized confirm
contract.
resources/js/pages/plugins/components/install.tsx (1)

11-18: ⚡ Quick win

Include variant in dialog.confirm.open payload for consistency.

Add explicit variant to match the standardized confirm contract.

🛠️ Suggested fix
         dialog.confirm.open({
           title: 'Install plugin',
           description: `Are you sure you want to install the plugin located at ${plugin.folder}?`,
+          variant: 'default',
           confirmLabel: 'Install',
           method: 'patch',
           url: route('plugins.install'),
           data: { id: plugin.id },
         })

As per coding guidelines: For simple confirm or destructive actions, use dialog.confirm.open() with title, description, variant, confirmLabel, method, and url.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/pages/plugins/components/install.tsx` around lines 11 - 18, Add
the missing variant property to the dialog.confirm.open call to match the
standardized confirm contract: update the payload passed to dialog.confirm.open
(the call that currently uses title, description, confirmLabel, method, url,
data with plugin.folder and plugin.id) to include variant (e.g., 'default' or
the appropriate variant used elsewhere) alongside the other fields so the
confirm dialog follows the required contract.
resources/js/pages/projects/components/invitations.tsx (1)

23-26: ⚡ Quick win

Use Inertia navigation for the internal accept route.

Replace window.location.href with router.visit() to keep navigation consistent with app routing behavior.

🔁 Suggested fix
+import { router } from '`@inertiajs/react`';
@@
           <DropdownMenuItem
             onSelect={(e) => {
               e.preventDefault();
-              window.location.href = `/settings/projects/${invitation.project_id}/invitations/accept`;
+              router.visit(`/settings/projects/${invitation.project_id}/invitations/accept`);
             }}
           >

As per coding guidelines: Use <Link> or router.visit() for internal navigation — never raw <a> tags for internal routes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/pages/projects/components/invitations.tsx` around lines 23 - 26,
Replace the direct window.location.href navigation in the onSelect handler with
Inertia's router.visit to preserve SPA routing: change the handler to call
router.visit(`/settings/projects/${invitation.project_id}/invitations/accept`)
and ensure you import the router (e.g. import { router } from '`@inertiajs/react`'
or use the useRouter hook) at the top of the file so internal navigation for the
onSelect uses Inertia routing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Tables/DeploymentTable.php`:
- Around line 14-27: Add PHPDoc blocks for the new DeploymentTable class and its
methods/properties: document the class with a brief description and tags (e.g.,
`@package` or `@author` if used), add method PHPDoc for query() and columns()
describing purpose, parameters (none) and return types (void/array), and
document the protected properties $tableSettings, $defaultSort and $perPage with
`@var` annotations; update DeploymentTable, query(), columns(), and the properties
in the class to include these PHPDoc blocks so the file conforms to the
repository PHP documentation rule.
- Line 35: The Column::data accessor for 'server_id' dereferences the nullable
site relation on the Deployment model and can throw when site is null; update
the accessor (the Column::data callback that takes Deployment $deployment) to
guard the nullable relationship (e.g., check $deployment->site or use PHP
null-safe access) and return null (or a safe default) when site is missing so
you don't call ->server_id on null.

In `@resources/js/components/dialogs/confirmation-dialog.tsx`:
- Around line 38-57: The current submit function is invoked via Button onClick
and lacks a FormEvent, so change the pattern to a real form submit: convert
submit into an onSubmit handler that accepts (e: FormEvent), call
e.preventDefault(), then run the same logic that calls
form.delete/post/patch/put and onOpenChange/onSuccess; attach this handler to
the form element as onSubmit and make the dialog action button a plain
type="submit" (remove its onClick). Update references to the existing submit
function in ConfirmationDialog and ensure the handler still uses the same
visitOptions and onOpenChange/onSuccess calls.

In `@resources/js/components/dialogs/php-extensions-dialog.tsx`:
- Around line 30-31: The current destructuring const [, php] =
Object.entries(configs.service.services).filter(([key]) => key === 'php')[0] ||
null can throw when no php entry exists; replace this with a safe lookup (use
.find or check the [0] result) to get the php entry before destructuring — e.g.
obtain phpEntry = Object.entries(configs.service.services).find(([key]) => key
=== 'php') and set php = phpEntry ? phpEntry[1] : null — then keep the existing
if (!php) return null; guard; update references to php accordingly in
php-extensions-dialog.tsx to avoid destructuring from null.

In `@resources/js/components/dialogs/php-ini-dialog.tsx`:
- Around line 62-64: The Monaco Editor's value is bound to query.data.ini which
causes user edits to be overwritten on rerenders; change the Editor's value prop
to use form.data.ini so edits persist (reference Editor and
form.setData/form.data.ini). Also adjust the hydration gate in the response
handling: replace the truthy check (if (response.data?.ini)) with a string-type
check (e.g., typeof response.data?.ini === 'string') so empty-string INI is
correctly hydrated into form.setData('ini', response.data.ini). Ensure these two
changes are applied where the Editor component and the response handling logic
are implemented.

In `@resources/js/components/dialogs/plugin-logs-dialog.tsx`:
- Around line 18-29: expandedItems currently tracks expansion by array index
(setExpandedItems, toggleExpanded) and Card components use index as key, which
breaks when the errors array reorders; change to use a stable identifier from
each error (e.g., error.id or a concatenation of unique fields like
error.timestamp + error.type) instead of index: update toggleExpanded to accept
an id (string | number), store that id in expandedItems Set, update any callers
to pass error.id, and change Card key from index to that same stable id so
expansion state follows the correct error item even after reordering or refresh.

In `@resources/js/pages/backups/components/columns.tsx`:
- Around line 20-29: The confirmation message can render "undefined" when
backup.database is null; update the target computation used by DropdownMenuItem
and dialog.confirm.open so it never yields undefined (e.g. in the expression
that defines target which currently uses backup.type === 'database' ?
backup.database?.name : backup.path). Replace that ternary with a guarded
fallback (use nullish coalescing) so for database backups you use
backup.database?.name ?? '(unknown database)' and for file backups use
backup.path ?? '(unknown path)', and keep dialog.confirm.open and the
title/description templates unchanged to consume this safe target.

In `@resources/js/pages/dns-providers/components/edit-dialog.tsx`:
- Around line 29-32: The form is initialized once with useForm({ name:
dnsProvider.name, global: dnsProvider.global }) so its state can drift when
dnsProvider changes; update the logic to reset the form whenever dnsProvider
changes by calling the form reset/update API (e.g., form.setValues or
form.reset) inside an effect that watches dnsProvider (or re-create the form by
keying the component on dnsProvider.id); locate the useForm call and ensure you
update/reset the form values from dnsProvider in an useEffect that runs when
dnsProvider changes.

In `@resources/js/pages/domains/components/record-form.tsx`:
- Around line 67-74: The onSuccess handlers for form.patch and form.post
currently only close the dialog (onOpenChange(false)) and can leave stale form
state; update both handlers to also reset or re-sync the form state (e.g., call
form.reset() and/or set form data to the current record/default values) before
or immediately after calling onOpenChange(false) so the next open starts with a
clean/accurate form (referencing the existing form.patch, form.post, and
onSuccess handlers to locate where to change).

In `@resources/js/pages/hosted-domains/components/create-hosted-domain.tsx`:
- Around line 30-36: The form's ssl_method default uses
site.webserver_default_ssl_method even when that value was filtered out by
allowedSslMethods, so change the initialization to pick a valid default from
sslMethodOptions: compute sslMethodOptions first (as you already do) and set
form's ssl_method to site.webserver_default_ssl_method only if sslMethodOptions
contains that value, otherwise fall back to a safe option such as 'none' or the
first sslMethodOptions[0].value; update the form initialization
(useForm<CreateForm>) to reference sslMethodOptions when determining the
ssl_method default to ensure the select starts with a permitted value.

In `@resources/js/pages/plugins/components/disable.tsx`:
- Line 14: The confirmation description currently uses plugin.name which may be
undefined; update the description in the disable dialog to use a null-safe label
(e.g. use plugin?.name ?? plugin?.id ?? a fallback like "this plugin") so the
copy never renders empty or "undefined"; change the string interpolation at the
description assignment (where plugin.name is used) to reference the safe
fallback expression.

In `@resources/js/pages/plugins/components/enable.tsx`:
- Around line 11-18: Update the dialog.confirm.open call to include an explicit
variant and make the description null-safe: add the variant property (e.g.,
variant: 'destructive' or 'default' per intent) and guard the plugin label by
using a fallback when plugin.name is missing (e.g., use plugin.name ?? 'this
plugin' or similar) in the description string; keep the existing confirmLabel,
method, url (route('plugins.enable')) and data ({ id: plugin.id }) intact so
dialog.confirm.open has title, description, variant, confirmLabel, method, url,
and data.

In `@resources/js/pages/plugins/components/update.tsx`:
- Line 13: The confirmation description uses plugin.name which can be undefined;
change the template in update.tsx (the description field that references
plugin.name) to reuse the uninstall action's fallback pattern by substituting
plugin.name with a safe fallback (e.g. plugin.name ?? plugin.slug ?? 'this
plugin') so the dialog never renders "undefined" and matches the uninstall
wording.

In `@resources/js/pages/server-providers/components/edit-dialog.tsx`:
- Around line 23-26: The form created by useForm only captures serverProvider at
mount, causing stale values when the dialog is reused; update the form whenever
serverProvider changes by adding an effect that resets or re-initializes the
form values (e.g., useEffect(() => form.setValues({ name: serverProvider.name,
global: serverProvider.global }), [serverProvider]) ) or recreate the form when
serverProvider changes (useMemo or recreate useForm with serverProvider as
dependency). Target the useForm call and the form instance (form.setValues /
form.reset / recreating useForm) to ensure the dialog always reflects the
current serverProvider.

In `@resources/js/pages/servers/components/actions.tsx`:
- Around line 65-72: The confirm dialog payloads for server actions are missing
the required variant field; update both dialog.confirm.open(...) calls (the
reboot/restart dialog and the other confirm at the second occurrence) to include
the project's standard variant (e.g., add variant: 'danger') alongside title,
description, confirmLabel, method, and url so they conform to the confirm
contract.

In `@resources/js/pages/services/components/config-file-dialog.tsx`:
- Around line 58-60: The Monaco Editor is bound to query.data.content causing
edits to be overwritten and the form is only hydrated when response.data.content
is truthy so empty config strings are ignored; change the Editor to use the form
state (use form.data.content as the value and call form.setData('content',
newValue) in onChange) so typing updates and persists in the form, and change
the hydration to set form.setData('content', response.data.content) when
response.data.content is defined (check for !== undefined/null rather than
truthiness) so empty-string configs are accepted; update references to Editor
value prop, onChange handler, and the hydration call that currently checks
response.data?.content.

In `@resources/js/stores/dialog-store.ts`:
- Around line 24-32: The close() function unconditionally focuses the previous
trigger on the next animation frame, which can steal focus if another dialog
opened in the meantime; modify close() (which uses triggerElement and set({
active: null })) to remember the dialog identity (or current active value)
before calling set, then inside requestAnimationFrame re-check that the dialog
store's active value is still the expected value (or null) and that
triggerElement still matches the captured trigger and isConnected before calling
focus(); this ensures focus is only restored when no other dialog became active
in the interim.

---

Outside diff comments:
In `@resources/js/pages/backups/components/edit-backup.tsx`:
- Around line 35-95: The Save button is bypassing the form-native submit by
using type="button" with onClick={submit}; change the Button (the one rendering
Save that references form="edit-backup-form") to use type="submit" and remove
its onClick handler so the Form's onSubmit={submit} (and its e.preventDefault()
logic) is used natively; ensure the Form id "edit-backup-form" and the submit
function remain unchanged so validation/processing (form.processing) still
controls the disabled state.

In `@resources/js/pages/cronjobs/components/columns.tsx`:
- Around line 28-33: The DropdownMenuItem currently prevents default in onSelect
and triggers submit on onClick which breaks keyboard activation; move the submit
invocation into the onSelect handler (e.g. call submit inside onSelect), remove
or adjust the existing onClick usage, and ensure you still call
e.preventDefault() if needed before calling submit so the
form.post(route(routeName, routeParams)) in submit runs for both mouse and
keyboard while respecting form.processing and showing LoaderCircleIcon.

In `@resources/js/pages/firewall/components/form.tsx`:
- Around line 43-54: The submit handler currently reuses the form instance
initialized once from firewallRule which allows stale values when the dialog is
reused; update the component to reset or reinitialize the form whenever the
active firewallRule or the dialog open state changes and also after a successful
submit. Concretely, add logic (e.g. useEffect or the dialog open handler) that
calls form.reset(...) or form.setData(...) with the current firewallRule fields
(or empty defaults when creating) when firewallRule changes or when opening the
dialog, and ensure the onSuccess callbacks for both form.put and form.post also
reset the form (and then call onOpenChange(false)) so the next open starts with
fresh data; reference the submit function, form.put, form.post, firewallRule and
onOpenChange to locate where to hook the reset.

In `@resources/js/pages/hosted-domains/components/edit-hosted-domain.tsx`:
- Around line 44-49: The form created with useForm<EditForm> is only seeded once
from hostedDomain so if the dialog is reused for another hostedDomain it can
show stale values; update the component to reset or reinitialize the form
whenever hostedDomain changes (e.g., in a useEffect that calls the form reset
method or re-creates form state) so fields (domain, type, ssl_method, ssl_id)
are updated for the new hostedDomain; apply the same approach for the other
instance referenced around lines 73-74 to ensure no state leaks between dialog
sessions.

In `@resources/js/pages/scripts/components/form.tsx`:
- Around line 16-26: ScriptForm's useForm is only initialized on mount so
reopening or switching script leaves stale fields and errors; add a useEffect in
the ScriptForm component that watches [script, open] and calls form.setData({
name: script?.name ?? '', content: script?.content ?? '' }) (and clear
errors/state if needed) to rehydrate the form when the modal is opened or the
script prop changes, and make the Monaco Editor controlled by switching from
defaultValue to value={form.data.content} (or remount it by giving it a key tied
to script?.id) so the editor content stays in sync with form.data.content.

In `@resources/js/pages/server-features/components/feature-action.tsx`:
- Line 68: The Button currently binds submit via onClick which can trigger
duplicate submissions; remove the onClick={submit} from the Button that
references form="action-form" and instead set type="submit" so the form's
onSubmit handles submission; ensure the existing submit handler (submit) is
wired to the form's onSubmit (not the Button) and keep
disabled={form.processing} intact to prevent double requests.

In `@resources/js/pages/site-features/components/feature-action.tsx`:
- Around line 28-43: The form state persists between dialog opens because the
useForm instance (form) is never reset; update the submit flow and dialog open
handling in feature-action.tsx so the form is cleared between uses: call
form.reset() after a successful post (inside submit's onSuccess before/after
calling onOpenChange(false)) and also clear form when the dialog is opened (add
an effect or handler that calls form.reset() when the dialog's open state
becomes true) to ensure no stale values remain; reference the useForm hook, the
submit function, form.post, and onOpenChange to locate where to add the reset
calls.

---

Nitpick comments:
In `@resources/js/components/dialogs/log-viewer-dialog.tsx`:
- Around line 32-34: Replace the raw anchor used for internal navigation with
Inertia's Link: change the <a href={route('logs.download', { server: serverId,
log: logId })} ...> wrapper around the Button to a Link from Inertia (e.g.
import { Link } from '`@inertiajs/react`' or '`@inertiajs/inertia-react`' depending
on the project), keeping the same href (route('logs.download', { server:
serverId, log: logId })) and attributes (target="_blank" rel="noopener
noreferrer") and ensure the Link wraps the Button component so the internal
route uses the router API rather than a raw <a>.

In `@resources/js/pages/domains/components/columns.tsx`:
- Line 7: Remove the now-unused router import from `@inertiajs/react` in
resources/js/pages/domains/components/columns.tsx: locate the import line that
includes "router" and delete "router" (or remove the entire import if nothing
else is imported) so there are no unused imports after the dialog refactor.

In `@resources/js/pages/php/components/default-cli.tsx`:
- Around line 12-19: Add the missing variant key to the dialog.confirm.open
payload to comply with the confirm contract: update the call to
dialog.confirm.open (in default-cli.tsx) to include a variant property (e.g.,
variant: 'confirm' or 'destructive' as appropriate for this action) alongside
title, description, confirmLabel, method, url and data so the dialog behavior
matches the project's standardized confirm contract.

In `@resources/js/pages/plugins/components/install.tsx`:
- Around line 11-18: Add the missing variant property to the dialog.confirm.open
call to match the standardized confirm contract: update the payload passed to
dialog.confirm.open (the call that currently uses title, description,
confirmLabel, method, url, data with plugin.folder and plugin.id) to include
variant (e.g., 'default' or the appropriate variant used elsewhere) alongside
the other fields so the confirm dialog follows the required contract.

In `@resources/js/pages/projects/components/invitations.tsx`:
- Around line 23-26: Replace the direct window.location.href navigation in the
onSelect handler with Inertia's router.visit to preserve SPA routing: change the
handler to call
router.visit(`/settings/projects/${invitation.project_id}/invitations/accept`)
and ensure you import the router (e.g. import { router } from '`@inertiajs/react`'
or use the useRouter hook) at the top of the file so internal navigation for the
onSelect uses Inertia routing.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d26bfa6-e8f2-4c2a-bb4e-fc8d53538b15

📥 Commits

Reviewing files that changed from the base of the PR and between f0bca3d and 277f6ca.

📒 Files selected for processing (101)
  • .github/instructions/frontend.instructions.md
  • CLAUDE.md
  • app/Enums/DeploymentStatus.php
  • app/Http/Controllers/ApplicationController.php
  • app/Tables/DeploymentTable.php
  • app/Tables/Servers/FirewallRuleTable.php
  • resources/js/components/dialogs/activate-server-ssl-dialog.tsx
  • resources/js/components/dialogs/confirmation-dialog.tsx
  • resources/js/components/dialogs/dialog-host.tsx
  • resources/js/components/dialogs/log-viewer-dialog.tsx
  • resources/js/components/dialogs/php-extensions-dialog.tsx
  • resources/js/components/dialogs/php-ini-dialog.tsx
  • resources/js/components/dialogs/plugin-logs-dialog.tsx
  • resources/js/components/dialogs/registry.ts
  • resources/js/components/dialogs/worker-logs-dialog.tsx
  • resources/js/components/server-banners.tsx
  • resources/js/hooks/use-dialog.ts
  • resources/js/layouts/app/layout.tsx
  • resources/js/pages/api-keys/components/columns.tsx
  • resources/js/pages/application/components/app-with-deployment.tsx
  • resources/js/pages/application/components/delete-deployment.tsx
  • resources/js/pages/application/components/deployment-columns.tsx
  • resources/js/pages/application/components/rollback.tsx
  • resources/js/pages/backups/components/columns.tsx
  • resources/js/pages/backups/components/edit-backup.tsx
  • resources/js/pages/backups/components/file-columns.tsx
  • resources/js/pages/backups/components/restore-backup.tsx
  • resources/js/pages/commands/components/columns.tsx
  • resources/js/pages/commands/components/edit-command.tsx
  • resources/js/pages/cronjobs/components/columns.tsx
  • resources/js/pages/cronjobs/components/form.tsx
  • resources/js/pages/cronjobs/index.tsx
  • resources/js/pages/database-users/components/columns.tsx
  • resources/js/pages/database-users/components/edit-database-user.tsx
  • resources/js/pages/database-users/components/link-dialog.tsx
  • resources/js/pages/databases/components/delete.tsx
  • resources/js/pages/dns-providers/components/delete.tsx
  • resources/js/pages/dns-providers/components/edit-dialog.tsx
  • resources/js/pages/dns-providers/components/edit.tsx
  • resources/js/pages/domains/components/columns.tsx
  • resources/js/pages/domains/components/record-columns.tsx
  • resources/js/pages/domains/components/record-form.tsx
  • resources/js/pages/domains/show.tsx
  • resources/js/pages/firewall/components/delete.tsx
  • resources/js/pages/firewall/components/form.tsx
  • resources/js/pages/firewall/index.tsx
  • resources/js/pages/hosted-domains/components/create-hosted-domain.tsx
  • resources/js/pages/hosted-domains/components/edit-hosted-domain.tsx
  • resources/js/pages/hosted-domains/index.tsx
  • resources/js/pages/monitoring/components/actions.tsx
  • resources/js/pages/monitoring/components/data-retention-dialog.tsx
  • resources/js/pages/notification-channels/components/delete.tsx
  • resources/js/pages/notification-channels/components/edit-dialog.tsx
  • resources/js/pages/notification-channels/components/edit.tsx
  • resources/js/pages/php/components/default-cli.tsx
  • resources/js/pages/php/components/extensions.tsx
  • resources/js/pages/php/components/ini.tsx
  • resources/js/pages/plugins/components/delete-logs.tsx
  • resources/js/pages/plugins/components/disable.tsx
  • resources/js/pages/plugins/components/enable.tsx
  • resources/js/pages/plugins/components/install.tsx
  • resources/js/pages/plugins/components/installed.tsx
  • resources/js/pages/plugins/components/uninstall.tsx
  • resources/js/pages/plugins/components/update.tsx
  • resources/js/pages/plugins/components/view-logs.tsx
  • resources/js/pages/projects/components/invitations.tsx
  • resources/js/pages/redirects/components/columns.tsx
  • resources/js/pages/scripts/components/columns.tsx
  • resources/js/pages/scripts/components/form.tsx
  • resources/js/pages/scripts/index.tsx
  • resources/js/pages/server-features/components/feature-action.tsx
  • resources/js/pages/server-features/index.tsx
  • resources/js/pages/server-logs/components/columns.tsx
  • resources/js/pages/server-providers/components/delete.tsx
  • resources/js/pages/server-providers/components/edit-dialog.tsx
  • resources/js/pages/server-providers/components/edit.tsx
  • resources/js/pages/server-ssh-keys/components/delete.tsx
  • resources/js/pages/server-ssls/components/activate-server-ssl.tsx
  • resources/js/pages/server-ssls/components/columns.tsx
  • resources/js/pages/servers/components/actions.tsx
  • resources/js/pages/servers/components/reboot-server.tsx
  • resources/js/pages/servers/components/update-server.tsx
  • resources/js/pages/services/components/action.tsx
  • resources/js/pages/services/components/config-file-dialog.tsx
  • resources/js/pages/services/components/config-file.tsx
  • resources/js/pages/services/components/installation-log.tsx
  • resources/js/pages/services/components/uninstall.tsx
  • resources/js/pages/site-features/components/feature-action.tsx
  • resources/js/pages/site-features/index.tsx
  • resources/js/pages/sites/stats.tsx
  • resources/js/pages/source-controls/components/columns.tsx
  • resources/js/pages/source-controls/components/edit-dialog.tsx
  • resources/js/pages/ssh-keys/components/delete.tsx
  • resources/js/pages/storage-providers/components/delete.tsx
  • resources/js/pages/storage-providers/components/edit-dialog.tsx
  • resources/js/pages/storage-providers/components/edit.tsx
  • resources/js/pages/workers/components/columns.tsx
  • resources/js/pages/workers/components/form.tsx
  • resources/js/pages/workers/components/worker-row-actions.tsx
  • resources/js/pages/workers/index.tsx
  • resources/js/stores/dialog-store.ts
💤 Files with no reviewable changes (5)
  • resources/js/pages/application/components/delete-deployment.tsx
  • resources/js/pages/servers/components/reboot-server.tsx
  • resources/js/pages/application/components/rollback.tsx
  • resources/js/pages/servers/components/update-server.tsx
  • resources/js/pages/application/components/deployment-columns.tsx

Comment thread app/Tables/DeploymentTable.php
Comment thread app/Tables/DeploymentTable.php
Comment thread resources/js/components/dialogs/confirmation-dialog.tsx
Comment thread resources/js/components/dialogs/php-extensions-dialog.tsx Outdated
Comment thread resources/js/components/dialogs/php-ini-dialog.tsx Outdated
Comment thread resources/js/pages/plugins/components/update.tsx Outdated
Comment thread resources/js/pages/server-providers/components/edit-dialog.tsx
Comment thread resources/js/pages/servers/components/actions.tsx
Comment thread resources/js/pages/services/components/config-file-dialog.tsx Outdated
Comment thread resources/js/stores/dialog-store.ts
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.

2 participants