From 8a5ad58f81ae41a97d69f691481d10b0d45ffa0b Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:17:35 +1100 Subject: [PATCH] chore(studio): replace CloseConfirmationModal with DiscardChangesConfirmationDialog (#43430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Form handling improvement. ## What is the current behavior? https://github.com/supabase/supabase/pull/43201/ standardised our discard changes behaviour with a shared hook and `DiscardChangesConfirmationDialog` component. But many forms and sheets still: 1. Don’t have any Discard-confirm close behaviour, making it too easy to make accidental discards 2. Use a more complicated, manually-created `CloseConfirmationModal` approach ## What is the new behavior? - Replaced all instances of `#2` above that had `CloseConfirmationModal` with `DiscardChangesConfirmationDialog` and its hook - Improved design system documentation around dirty form dismissal | Before | After | | --- | --- | | Mercor Apexroles Foo
Supabase-9A40EC7C-F335-4B26-B567-450FC0845463 | Mercor Apexroles Foo
Supabase-F427F1FA-DECC-4194-B663-A9E5A6F285A1 | ## To test Try editing values these sheets in staging, then blurring the sheet or pressing `esc`: - CreateQueueSheet.tsx - CronJobsTab.tsx - CronJobPage.tsx - EditWrapperSheet.tsx - OverviewTab.tsx - WrappersTab.tsx - CreateFunction/index.tsx - EditHookPanel.tsx - TriggerSheet.tsx - SidePanelEditor.tsx - EditSecretSheet.tsx - PolicyEditorModal/index.tsx - PolicyEditorPanel/index.tsx ## Still to come - [ ] Incrementally take on `#1`: implement `DiscardChangesConfirmationDialog` and its hook in sheets or dialog forms that have no dirty form dismissal handling --------- Co-authored-by: Joshen Lim --- apps/design-system/__registry__/index.tsx | 11 + .../docs/fragments/confirmation-modal.mdx | 2 +- .../content/docs/ui-patterns/modality.mdx | 78 +-- .../example/sheet-confirm-on-close-demo.tsx | 225 +++++++++ apps/design-system/registry/examples.ts | 6 + .../Auth/Policies/Policies.utils.ts | 10 +- .../Auth/Policies/PolicyEditor/index.tsx | 4 +- .../Auth/Policies/PolicyEditorModal/index.tsx | 39 +- .../Auth/Policies/PolicyEditorPanel/index.tsx | 28 +- .../interfaces/Auth/Policies/PolicyReview.tsx | 2 - .../Functions/CreateFunction/index.tsx | 25 +- .../Database/Hooks/EditHookPanel.tsx | 23 +- .../Database/Triggers/TriggerSheet.tsx | 41 +- .../EdgeFunctionSecrets/EditSecretSheet.tsx | 223 ++++----- .../CreateCronJobSheet/CreateCronJobSheet.tsx | 463 +++++++++--------- .../Integrations/CronJobs/CronJobPage.tsx | 56 +-- .../Integrations/CronJobs/CronJobsTab.tsx | 45 +- .../Integrations/Queues/CreateQueueSheet.tsx | 25 +- .../Wrappers/EditWrapperSheet.tsx | 22 +- .../Integrations/Wrappers/OverviewTab.tsx | 25 +- .../Integrations/Wrappers/WrappersTab.tsx | 27 +- .../StoragePolicies/StoragePolicies.tsx | 16 +- .../StoragePoliciesEditPolicyModal.tsx | 12 +- .../SidePanelEditor/SidePanelEditor.tsx | 23 +- .../DiscardChangesConfirmationDialog.tsx | 8 +- 25 files changed, 718 insertions(+), 721 deletions(-) create mode 100644 apps/design-system/registry/default/example/sheet-confirm-on-close-demo.tsx diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index a4e25e570bf..412a1c26944 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -1292,6 +1292,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "sheet-confirm-on-close-demo": { + name: "sheet-confirm-on-close-demo", + type: "components:example", + registryDependencies: ["alert-dialog","button","input","label","separator","sheet"], + component: React.lazy(() => import("@/registry/default/example/sheet-confirm-on-close-demo")), + source: "", + files: ["registry/default/example/sheet-confirm-on-close-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "sheet-demo": { name: "sheet-demo", type: "components:example", diff --git a/apps/design-system/content/docs/fragments/confirmation-modal.mdx b/apps/design-system/content/docs/fragments/confirmation-modal.mdx index e613c83444f..ac52d9beb11 100644 --- a/apps/design-system/content/docs/fragments/confirmation-modal.mdx +++ b/apps/design-system/content/docs/fragments/confirmation-modal.mdx @@ -10,7 +10,7 @@ Use Confirmation Modal when the user needs extra context to make a decision, suc If the confirmation can be expressed as a single short paragraph, use [Alert Dialog](../components/alert-dialog). If the action is highly destructive and requires explicit typed intent, use [Text Confirm Dialog](../fragments/text-confirm-dialog). See [Modality](../ui-patterns/modality) for broader guidance on choosing the appropriate pattern. -For dirty-form dismissal in dialogs/sheets, prefer the dedicated discard-confirmation pattern (`DiscardChangesConfirmationDialog` + `useConfirmOnClose`) rather than creating new local `CloseConfirmationModal` wrappers. The ad-hoc `CloseConfirmationModal` wrapper pattern is deprecated for new implementations. +For dirty-form dismissal in dialogs/sheets, use the dedicated discard-confirmation pattern (`DiscardChangesConfirmationDialog` + `useConfirmOnClose`) instead of `ConfirmationModal`. Avoid creating custom wrapper components for this flow; wire `modalProps` from `useConfirmOnClose` directly into `DiscardChangesConfirmationDialog`. diff --git a/apps/design-system/content/docs/ui-patterns/modality.mdx b/apps/design-system/content/docs/ui-patterns/modality.mdx index 83683b10874..eca2d40bdce 100644 --- a/apps/design-system/content/docs/ui-patterns/modality.mdx +++ b/apps/design-system/content/docs/ui-patterns/modality.mdx @@ -18,18 +18,6 @@ We have two main ways of handling modality: As a general rule: use dialogs for short, focused tasks and use sheets for longer forms or more detailed views. -### Dirty form dismissal pattern - -When a dialog or sheet contains a form, users should generally be allowed to attempt dismissal via all normal affordances (backdrop click, Escape key, close icon, and footer `Cancel` button). - -If the form is clean, close immediately. If the form has unsaved changes, show a discard-confirmation dialog instead of closing immediately. - -This pattern is implemented in Studio with `useConfirmOnClose` plus `DiscardChangesConfirmationDialog`. - -- **Prompt on footer `Cancel` too:** If a button is labeled `Cancel`, users expect it to stop the current action, not silently discard edits. Prompting keeps behavior consistent with backdrop/Escape dismissal and prevents accidental loss. -- **Use explicit labels for no-prompt discard:** If you intentionally want a one-click destructive exit, label the action `Discard` (or `Discard changes`) rather than `Cancel`. -- **Guard close attempts, not unmounts:** This pattern should intercept controlled modal/sheet close attempts (`onOpenChange`, close buttons, footer actions). It should not attempt to block route changes or arbitrary component unmounts. - ## Dialogs Dialogs are centered overlays used for short, focused tasks. All dialogs should follow these best practices: @@ -53,22 +41,6 @@ There are quite a few dialog components, each suited to a different task or cont -#### Discard changes confirmation dialog (pattern) - -For dirty form dismissal, use a short discard-confirmation dialog after a close attempt instead of disabling dismissal entirely. - -- The primary form remains in a `Dialog` or `Sheet` (dismissible). -- Closing is intercepted only when the form is dirty. -- The follow-up confirmation is an `AlertDialog` pattern (`DiscardChangesConfirmationDialog` in Studio). - -Typical flow: - -1. User attempts to close the dialog/sheet (backdrop, Escape, close icon, or `Cancel`) -2. If the form is clean, close immediately -3. If the form is dirty, show discard confirmation -4. `Keep editing` returns to the form -5. `Discard changes` closes and resets the form - #### Text Confirm Dialog [Text Confirm Dialog](../fragments/text-confirm-dialog) adds a deliberate speed bump for highly destructive actions by requiring the user to type an exact confirmation string before proceeding. The confirm action remains disabled until the input matches. @@ -102,3 +74,53 @@ Sheets are dialogs presented as side panels. Use them for content that is larger [Sheet](../components/sheet) is modal by default, blocking interaction with the underlying page. + +## Best practices + +### Dirty form dismissal + +When a dialog or sheet contains a form, keep all normal dismissal affordances enabled (backdrop click, Escape key, close icon, and footer `Cancel` button). + +Decision flow: + +1. User attempts to close the dialog/sheet. +2. If the form is clean, close immediately. +3. If the form is dirty, show a discard-confirmation dialog. +4. `Keep editing` returns to the form. +5. `Discard changes` closes and resets the form. + +Implementation checklist: + +- Intercept close attempts from `onOpenChange`. +- Route footer `Cancel` through the same close guard. +- Render a separate discard confirmation dialog when dirty. +- Keep `Cancel` non-destructive; use `Discard`/`Discard changes` for one-click destructive exits. +- Guard controlled close attempts only; do not try to block route changes or arbitrary unmounts. + +Studio implementation (preferred in Studio code): + +```tsx +import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' +import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' + +const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ + checkIsDirty: () => form.formState.isDirty, + onClose, +}) + + + ... + + ... + + +``` + +Generic implementation (outside Studio): + +- If Studio-only helpers are unavailable, recreate the same behavior with `AlertDialog`. +- The demo below shows the same flow and API shape (`confirmOnClose`, `handleOpenChange`, `modalProps`). + + diff --git a/apps/design-system/registry/default/example/sheet-confirm-on-close-demo.tsx b/apps/design-system/registry/default/example/sheet-confirm-on-close-demo.tsx new file mode 100644 index 00000000000..a82d115797a --- /dev/null +++ b/apps/design-system/registry/default/example/sheet-confirm-on-close-demo.tsx @@ -0,0 +1,225 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + Button, + Input_Shadcn_ as Input, + Label_Shadcn_ as Label, + Separator, + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetSection, + SheetTitle, +} from 'ui' + +interface EndpointValues { + endpointUrl: string + secretHeader: string +} + +interface ConfirmOnCloseModalProps { + visible: boolean + onClose: () => void + onCancel: () => void +} + +const defaultValues: EndpointValues = { + endpointUrl: '', + secretHeader: '', +} + +const useConfirmOnClose = ({ + checkIsDirty, + onClose, +}: { + checkIsDirty: () => boolean + onClose: () => void +}) => { + const [visible, setVisible] = useState(false) + + const confirmOnClose = useCallback(() => { + if (checkIsDirty()) { + setVisible(true) + return + } + + onClose() + }, [checkIsDirty, onClose]) + + const handleOpenChange = useCallback( + (open: boolean) => { + if (!open) { + confirmOnClose() + } + }, + [confirmOnClose] + ) + + const onConfirm = useCallback(() => { + setVisible(false) + onClose() + }, [onClose]) + + const onCancel = useCallback(() => { + setVisible(false) + }, []) + + const modalProps: ConfirmOnCloseModalProps = useMemo( + () => ({ + visible, + onClose: onConfirm, + onCancel, + }), + [visible, onConfirm, onCancel] + ) + + return { + confirmOnClose, + handleOpenChange, + modalProps, + } +} + +const DiscardChangesAlertDialog = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => { + const isConfirmingRef = useRef(false) + + useEffect(() => { + if (visible) { + isConfirmingRef.current = false + } + }, [visible]) + + const handleConfirm = useCallback(() => { + isConfirmingRef.current = true + onClose() + }, [onClose]) + + const handleOpenChange = useCallback( + (open: boolean) => { + if (open) return + + if (isConfirmingRef.current) { + isConfirmingRef.current = false + return + } + + onCancel() + }, + [onCancel] + ) + + return ( + + + + Discard changes? + + Any unsaved changes to this endpoint will be lost. + + + + Keep editing + + Discard changes + + + + + ) +} + +export default function SheetConfirmOnCloseDemo() { + const [open, setOpen] = useState(false) + const [savedValues, setSavedValues] = useState(defaultValues) + const [draftValues, setDraftValues] = useState(defaultValues) + + const isDirty = useMemo( + () => + draftValues.endpointUrl !== savedValues.endpointUrl || + draftValues.secretHeader !== savedValues.secretHeader, + [draftValues, savedValues] + ) + + const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ + checkIsDirty: () => isDirty, + onClose: () => { + setDraftValues(savedValues) + setOpen(false) + }, + }) + + const openSheet = () => { + setDraftValues(savedValues) + setOpen(true) + } + + const saveChanges = () => { + setSavedValues(draftValues) + setOpen(false) + } + + return ( + <> + + + + + + Edit endpoint + + + +
+ + + setDraftValues((current) => ({ + ...current, + endpointUrl: event.target.value, + })) + } + /> +
+
+ + + setDraftValues((current) => ({ + ...current, + secretHeader: event.target.value, + })) + } + /> +
+
+ + + + + +
+
+ + + ) +} diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index 905e07d95cf..3a236c19096 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -761,6 +761,12 @@ export const examples: Registry = [ registryDependencies: ['separator'], files: ['example/separator-demo.tsx'], }, + { + name: 'sheet-confirm-on-close-demo', + type: 'components:example', + registryDependencies: ['alert-dialog', 'button', 'input', 'label', 'separator', 'sheet'], + files: ['example/sheet-confirm-on-close-demo.tsx'], + }, { name: 'sheet-demo', type: 'components:example', diff --git a/apps/studio/components/interfaces/Auth/Policies/Policies.utils.ts b/apps/studio/components/interfaces/Auth/Policies/Policies.utils.ts index c741b45ab07..f96ba267ee1 100644 --- a/apps/studio/components/interfaces/Auth/Policies/Policies.utils.ts +++ b/apps/studio/components/interfaces/Auth/Policies/Policies.utils.ts @@ -1,10 +1,10 @@ -import type { PostgresPolicy } from '@supabase/postgres-meta' -import { has, isEmpty, isEqual } from 'lodash' - import { ident } from '@supabase/pg-meta/src/pg-format' +import type { PostgresPolicy } from '@supabase/postgres-meta' import { generateSqlPolicy } from 'data/ai/sql-policy-mutation' import type { CreatePolicyBody } from 'data/database-policies/database-policy-create-mutation' import type { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query' +import { has, isEmpty, isEqual } from 'lodash' + import { PolicyFormField, PolicyForReview, @@ -19,7 +19,7 @@ import { export const createSQLPolicy = ( policyFormFields: PolicyFormField, - originalPolicyFormFields: PostgresPolicy + originalPolicyFormFields?: PostgresPolicy ) => { const { definition, check } = policyFormFields const formattedPolicyFormFields = { @@ -32,7 +32,7 @@ export const createSQLPolicy = ( check: check ? check.replace(/\s+/g, ' ').trim() : check === undefined ? null : check, } - if (isEmpty(originalPolicyFormFields)) { + if (!originalPolicyFormFields || isEmpty(originalPolicyFormFields)) { return createSQLStatementForCreatePolicy(formattedPolicyFormFields) } diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/index.tsx index a1ec84172dc..e004ff673b0 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/index.tsx @@ -14,7 +14,7 @@ interface PolicyEditorProps { onReviewPolicy: () => void } -const PolicyEditor = ({ +export const PolicyEditor = ({ isNewPolicy = true, policyFormFields = {}, onUpdatePolicyFormFields = () => {}, @@ -75,5 +75,3 @@ const PolicyEditor = ({ ) } - -export default PolicyEditor diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx index 8460189bed3..5fbd96084c3 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx @@ -1,15 +1,17 @@ +import { PostgresPolicy } from '@supabase/postgres-meta' import { useFeaturePreviewModal } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import useLatest from 'hooks/misc/useLatest' -import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' +import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' import { isEmpty, noop } from 'lodash' import { useCallback, useEffect, useState } from 'react' import { toast } from 'sonner' import { Modal } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { POLICY_MODAL_VIEWS } from '../Policies.constants' import { PolicyFormField, + PolicyForReview, PostgresPolicyCreatePayload, PostgresPolicyUpdatePayload, } from '../Policies.types' @@ -18,7 +20,7 @@ import { createPayloadForUpdatePolicy, createSQLPolicy, } from '../Policies.utils' -import PolicyEditor from '../PolicyEditor' +import { PolicyEditor } from '../PolicyEditor' import { PolicyReview } from '../PolicyReview' import PolicySelection from '../PolicySelection' import PolicyTemplates from '../PolicyTemplates' @@ -30,7 +32,7 @@ interface PolicyEditorModalProps { visible?: boolean schema?: string table?: string - selectedPolicyToEdit: any + selectedPolicyToEdit?: PostgresPolicy showAssistantPreview?: boolean onSelectCancel: () => void onCreatePolicy: (payload: PostgresPolicyCreatePayload) => Promise @@ -38,11 +40,11 @@ interface PolicyEditorModalProps { onSaveSuccess: () => void } -const PolicyEditorModal = ({ +export const PolicyEditorModal = ({ visible = false, schema = '', table = '', - selectedPolicyToEdit = {}, + selectedPolicyToEdit, showAssistantPreview = false, onSelectCancel = noop, onCreatePolicy, @@ -71,10 +73,10 @@ const PolicyEditorModal = ({ const [policyFormFields, setPolicyFormFields] = useState( initializedPolicyFormFields ) - const [policyStatementForReview, setPolicyStatementForReview] = useState('') + const [policyStatementForReview, setPolicyStatementForReview] = useState() const [isDirty, setIsDirty] = useState(false) - const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ + const { confirmOnClose, modalProps } = useConfirmOnClose({ checkIsDirty: () => isDirty, onClose: () => { onSelectCancel() @@ -198,7 +200,7 @@ const PolicyEditorModal = ({ onCancel={confirmOnClose} >
- + {view === POLICY_MODAL_VIEWS.SELECTION ? ( - ) : view === POLICY_MODAL_VIEWS.REVIEW ? ( + ) : view === POLICY_MODAL_VIEWS.REVIEW && !!policyStatementForReview ? ( ) } - -const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => ( - -

- There are unsaved changes. Are you sure you want to close the editor? Your changes will be - lost. -

-
-) - -export default PolicyEditorModal diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx index 27ff753378a..511257ab827 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx @@ -12,12 +12,13 @@ import * as z from 'zod' import { useParams } from 'common' import { IStandaloneCodeEditor } from 'components/interfaces/SQLEditor/SQLEditor.types' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { useDatabasePolicyUpdateMutation } from 'data/database-policies/database-policy-update-mutation' import { databasePoliciesKeys } from 'data/database-policies/keys' import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' +import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' import { Button, Checkbox_Shadcn_, @@ -33,7 +34,6 @@ import { Tabs_Shadcn_, cn, } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { LockedCreateQuerySection, LockedRenameQuerySection } from './LockedQuerySection' import { PolicyDetailsV2 } from './PolicyDetailsV2' import { checkIfPolicyHasChanged, generateCreatePolicyQuery } from './PolicyEditorPanel.utils' @@ -165,7 +165,7 @@ export const PolicyEditorPanel = memo(function ({ return policyCreateUnsaved || policyUpdateUnsaved }, [command, name, roles, selectedPolicy]) - const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ + const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ checkIsDirty: hasUnsavedChanges, onClose: onSelectCancel, }) @@ -292,7 +292,7 @@ export const PolicyEditorPanel = memo(function ({ <>
- + - + ) }) PolicyEditorPanel.displayName = 'PolicyEditorPanel' - -const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => ( - -

- Are you sure you want to close the editor? Any unsaved changes on your policy and - conversations with the Assistant will be lost. -

-
-) diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyReview.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyReview.tsx index f6cdc8f45ec..955566e3904 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyReview.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyReview.tsx @@ -64,5 +64,3 @@ export const PolicyReview = ({ ) } - -export default PolicyReview diff --git a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx index b28b5eb7221..77727b3a454 100644 --- a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx +++ b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx @@ -13,7 +13,7 @@ import { useDatabaseFunctionCreateMutation } from 'data/database-functions/datab import { DatabaseFunction } from 'data/database-functions/database-functions-query' import { useDatabaseFunctionUpdateMutation } from 'data/database-functions/database-functions-update-mutation' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' +import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' import { useProtectedSchemas } from 'hooks/useProtectedSchemas' import type { FormSchema } from 'types' import { @@ -41,7 +41,7 @@ import { Toggle, cn, } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { convertArgumentTypes, convertConfigParams } from '../Functions.utils' import { CreateFunctionHeader } from './CreateFunctionHeader' @@ -87,7 +87,7 @@ export const CreateFunction = ({ }) const language = form.watch('language') - const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ + const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ checkIsDirty: () => form.formState.isDirty, onClose, }) @@ -158,7 +158,7 @@ export const CreateFunction = ({ const { data: protectedSchemas } = useProtectedSchemas() return ( - +
- + ) } -const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => ( - -

- There are unsaved changes. Are you sure you want to close the panel? Your changes will be - lost. -

-
-) - interface FormFieldConfigParamsProps { readonly?: boolean } diff --git a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx index 9c078d71c88..11646d724e7 100644 --- a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx @@ -8,16 +8,16 @@ import { useEffect, useRef, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, Form_Shadcn_, SidePanel } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormSchema, WebhookFormValues } from './EditHookPanel.constants' import { FormContents } from './FormContents' +import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { useDatabaseTriggerCreateMutation } from '@/data/database-triggers/database-trigger-create-mutation' import { useDatabaseTriggerUpdateMutation } from '@/data/database-triggers/database-trigger-update-transaction-mutation' import { useDatabaseHooksQuery } from '@/data/database-triggers/database-triggers-query' import { tableEditorQueryOptions } from '@/data/table-editor/table-editor-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { useConfirmOnClose, type ConfirmOnCloseModalProps } from '@/hooks/ui/useConfirmOnClose' +import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' import { uuidv4 } from '@/lib/helpers' export type HTTPArgument = { id: string; name: string; value: string } @@ -288,7 +288,7 @@ export const EditHookPanel = () => { // This is intentionally kept outside of the useConfirmOnClose hook to force RHF to update the isDirty state. const isDirty = form.formState.isDirty - const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ + const { confirmOnClose, modalProps } = useConfirmOnClose({ checkIsDirty: () => isDirty, onClose: () => onClose(), }) @@ -340,22 +340,7 @@ export const EditHookPanel = () => { - + ) } - -const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => ( - -

- There are unsaved changes. Are you sure you want to close the panel? Your changes will be - lost. -

-
-) diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx index c14145482fd..28fe906b51a 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx @@ -1,18 +1,17 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { Terminal } from 'lucide-react' -import { useEffect, useState } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' -import { toast } from 'sonner' -import * as z from 'zod' - import { PostgresTrigger } from '@supabase/postgres-meta' +import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import FormBoxEmpty from 'components/ui/FormBoxEmpty' import { useDatabaseTriggerCreateMutation } from 'data/database-triggers/database-trigger-create-mutation' import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/database-trigger-update-mutation' import { useTablesQuery } from 'data/tables/tables-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' +import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' import { useProtectedSchemas } from 'hooks/useProtectedSchemas' +import { Terminal } from 'lucide-react' +import { useEffect, useState } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'sonner' import { Button, Checkbox_Shadcn_, @@ -33,8 +32,9 @@ import { SheetHeader, SheetTitle, } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import * as z from 'zod' + import ChooseFunctionForm from './ChooseFunctionForm' import { TRIGGER_ENABLED_MODES, @@ -135,7 +135,7 @@ export const TriggerSheet = ({ }) const { function_name, function_schema } = form.watch() - const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ + const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ checkIsDirty: () => form.formState.isDirty, onClose, }) @@ -189,7 +189,7 @@ export const TriggerSheet = ({ return ( <> - + @@ -481,7 +481,7 @@ export const TriggerSheet = ({ - + @@ -489,25 +489,10 @@ export const TriggerSheet = ({ visible={showFunctionSelector} setVisible={setShowFunctionSelector} onChange={(fn) => { - form.setValue('function_name', fn.name) - form.setValue('function_schema', fn.schema) + form.setValue('function_name', fn.name, { shouldDirty: true }) + form.setValue('function_schema', fn.schema, { shouldDirty: true }) }} /> ) } - -const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => ( - -

- There are unsaved changes. Are you sure you want to close the panel? Your changes will be - lost. -

-
-) diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.tsx index a099b64198b..8863864d593 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.tsx @@ -1,27 +1,22 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { useEffect, useState, type ReactNode } from 'react' -import { SubmitHandler, useForm, type UseFormReturn } from 'react-hook-form' -import { toast } from 'sonner' -import z from 'zod' - import { useParams } from 'common' import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { useSecretsCreateMutation } from 'data/secrets/secrets-create-mutation' import { ProjectSecret } from 'data/secrets/secrets-query' import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' -import { Eye, EyeOff, X } from 'lucide-react' +import { Eye, EyeOff } from 'lucide-react' +import { useEffect, useState } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' import { useLatest } from 'react-use' +import { toast } from 'sonner' import { Button, - cn, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input, Input_Shadcn_, - Separator, Sheet, - SheetClose, SheetContent, SheetFooter, SheetHeader, @@ -29,6 +24,7 @@ import { SheetTitle, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import z from 'zod' const FORM_ID = 'edit-secret-sidepanel' @@ -46,22 +42,17 @@ interface EditSecretSheetProps { } export function EditSecretSheet({ secret, visible, onClose }: EditSecretSheetProps) { + const { ref: projectRef } = useParams() const secretName = useLatest(secret?.name) + const [showSecretValue, setShowSecretValue] = useState(false) + const form = useForm({ resolver: zodResolver(FormSchema), }) - useEffect(() => { - if (visible) { - form.reset({ - name: secretName.current ?? '', - value: '', - }) - } - }, [form, secretName, visible]) + const isValid = form.formState.isValid const isDirty = form.formState.isDirty - const { ref: projectRef } = useParams() const { mutate: updateSecret, isPending: isUpdating } = useSecretsCreateMutation({ onSuccess: (_, variables) => { toast.success(`Successfully updated secret "${variables.secrets[0].name}"`) @@ -75,25 +66,83 @@ export function EditSecretSheet({ secret, visible, onClose }: EditSecretSheetPro }) } - const { - confirmOnClose, - handleOpenChange, - modalProps: closeConfirmationModalProps, - } = useConfirmOnClose({ + const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ checkIsDirty: () => isDirty, onClose, }) + useEffect(() => { + if (visible) { + form.reset({ name: secretName.current ?? '', value: '' }) + } + }, [form, secretName, visible]) + return ( - -
- - + + + Edit secret + + + + +
+ ( + + + + + + )} + /> + ( + + + +
- + + ) } - -const Header = (): ReactNode => { - return ( - - - - Close - - Edit secret - - ) -} - -type FormBodyProps = { - form: UseFormReturn - onSubmit: SubmitHandler -} - -const FormBody = ({ form, onSubmit }: FormBodyProps): ReactNode => { - return ( - -
- - - - - - - - - -
- ) -} - -type NameFieldProps = { - form: UseFormReturn -} - -const NameField = ({ form }: NameFieldProps): ReactNode => { - return ( - ( - - - - - - )} - /> - ) -} - -type SecretFieldProps = { - form: UseFormReturn -} - -const SecretField = ({ form }: SecretFieldProps): ReactNode => { - const [showSecretValue, setShowSecretValue] = useState(false) - - return ( - ( - - - - - - - + {cronType === 'edge_function' && ( + <> + + + + + + + )} + {cronType === 'sql_function' && } + {cronType === 'sql_snippet' && } + + + + + + + + + + + {pgNetExtension && ( { const edgeFunctionSlug = edgeFunction?.split('/functions/v1/').pop() const isValidEdgeFunction = edgeFunctions.some((x) => x.slug === edgeFunctionSlug) - const [isDirty, setIsDirty] = useState(false) - const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ - checkIsDirty: () => isDirty, - onClose: () => { - setIsDirty(false) - setIsEditSheetOpen(false) - }, - }) - const pageTitle = childLabel || childId || 'Cron Job' const pageSubtitle = job ? ( @@ -192,40 +179,13 @@ export const CronJobPage = () => { - - - {job && ( - setIsEditSheetOpen(false)} - onCloseWithConfirmation={confirmOnClose} - /> - )} - - - + {job && ( + setIsEditSheetOpen(false)} + /> + )} ) } - -const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => ( - -

- There are unsaved changes. Are you sure you want to close the panel? Your changes will be - lost. -

-
-) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx index 807d37b8a8e..b3547a4d70e 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx @@ -5,7 +5,6 @@ import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-ex import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' import { cleanPointerEventsNoneOnBody, isAtBottom } from 'lib/helpers' import { createNavigationHandler } from 'lib/navigation' import { isGreaterThanOrEqual } from 'lib/semver' @@ -14,8 +13,7 @@ import { useRouter } from 'next/router' import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs' import { MouseEvent, UIEvent, useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' -import { LoadingLine, Sheet, SheetContent } from 'ui' -import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' +import { LoadingLine } from 'ui' import { formatCronJobColumns } from './CronJobs.utils' import { CronJobRunDetailsOverflowNotice } from './CronJobsTab.CleanupNotice' @@ -34,7 +32,6 @@ export const CronjobsTab = () => { const [searchQuery, setSearchQuery] = useQueryState('search', parseAsString.withDefault('')) - const [isDirty, setIsDirty] = useState(false) const [search, setSearch] = useState(searchQuery) const handleSearchSubmit = () => { @@ -142,13 +139,6 @@ export const CronjobsTab = () => { setCreateCronJobSheetShown(false) cleanPointerEventsNoneOnBody(500) } - const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ - checkIsDirty: () => isDirty, - onClose: () => { - setIsDirty(false) - onClose() - }, - }) useEffect(() => { if (grid.isSuccess && !!cronJobIdForEditing && !cronJobForEditing) { @@ -192,18 +182,11 @@ export const CronjobsTab = () => { - - - - - - + ) } @@ -228,19 +211,3 @@ const CronJobsFooter = ({ count }: CronJobsFooterProps) => ( )} ) - -// Confirmation modal for unsaved changes -const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => ( - -

- There are unsaved changes. Are you sure you want to close the panel? Your changes will be - lost. -

-
-) diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx index 95bc4ddc696..b01387918a1 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx @@ -8,7 +8,7 @@ import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-ex import { useDatabaseQueueCreateMutation } from 'data/database-queues/database-queues-create-mutation' import { useQueuesExposePostgrestStatusQuery } from 'data/database-queues/database-queues-expose-postgrest-status-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' +import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' import { useRouter } from 'next/router' import { useEffect } from 'react' import { @@ -31,7 +31,7 @@ import { SheetTitle, } from 'ui' import { Admonition } from 'ui-patterns' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { QUEUE_TYPES } from './Queues.constants' import { QueryNameSchema } from './Queues.utils' @@ -102,7 +102,7 @@ export const CreateQueueSheet = ({ visible, onClose }: CreateQueueSheetProps) => const checkIsDirty = () => form.formState.isDirty - const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ + const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ checkIsDirty, onClose, }) @@ -144,7 +144,7 @@ export const CreateQueueSheet = ({ visible, onClose }: CreateQueueSheetProps) => const queueType = form.watch('values.type') return ( - +
@@ -322,23 +322,8 @@ export const CreateQueueSheet = ({ visible, onClose }: CreateQueueSheetProps) =>
- +
) } - -const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => ( - -

- There are unsaved changes. Are you sure you want to close the panel? Your changes will be - lost. -

-
-) diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx index 42cf788d560..85cbb6afd22 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx @@ -11,8 +11,9 @@ import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation' import { FDW } from 'data/fdw/fdws-query' import { getDecryptedValues } from 'data/vault/vault-secret-decrypted-value-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' +import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' import { Button, Form, Input, SheetFooter, SheetHeader, SheetTitle } from 'ui' +import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import InputField from './InputField' import { WrapperMeta } from './Wrappers.types' @@ -108,7 +109,7 @@ export const EditWrapperSheet = ({ const checkIsDirty = useCallback(() => hasChangesRef.current, []) - const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ + const { confirmOnClose, modalProps } = useConfirmOnClose({ checkIsDirty, onClose, }) @@ -397,7 +398,7 @@ export const EditWrapperSheet = ({

Are you sure you want to continue?

- + ) } - -const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => ( - -

- There are unsaved changes. Are you sure you want to close the panel? Your changes will be - lost. -

-
-) diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx index ecc85736351..6560414d392 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx @@ -5,10 +5,11 @@ import { parseAsBoolean, useQueryState } from 'nuqs' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' +import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' import { Alert_Shadcn_, AlertDescription_Shadcn_, @@ -19,7 +20,6 @@ import { SheetContent, WarningIcon, } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab' import { CreateIcebergWrapperSheet } from './CreateIcebergWrapperSheet' import { CreateWrapperSheet } from './CreateWrapperSheet' @@ -45,7 +45,7 @@ export const WrapperOverviewTab = () => { }) const [isDirty, setIsDirty] = useState(false) - const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ + const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ checkIsDirty: () => isDirty, onClose: () => { setCreateWrapperShown(false) @@ -141,7 +141,7 @@ export const WrapperOverviewTab = () => { - + { /> - + ) } - -const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => ( - -

- There are unsaved changes. Are you sure you want to close the panel? Your changes will be - lost. -

-
-) diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx index 50eacdefe4c..2db4fa85d34 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx @@ -4,10 +4,10 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useFDWsQuery } from 'data/fdw/fdws-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' +import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' import { HTMLProps, ReactNode, useCallback, useState } from 'react' import { Sheet, SheetContent } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { CreateWrapperSheet } from './CreateWrapperSheet' import { WRAPPERS } from './Wrappers.constants' @@ -38,7 +38,7 @@ export const WrappersTab = () => { : [] const [isDirty, setIsDirty] = useState(false) - const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ + const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ checkIsDirty: useCallback(() => isDirty, [isDirty]), onClose: useCallback(() => { setCreateWrapperShown(false) @@ -50,7 +50,7 @@ export const WrappersTab = () => { ({ ...props }: { children: ReactNode } & HTMLProps) => (
{props.children} - + {wrapperMeta && ( {
), - [createWrapperShown, wrapperMeta, confirmOnClose] + [createWrapperShown, handleOpenChange, wrapperMeta, confirmOnClose] ) if (!wrapperMeta) { @@ -102,22 +102,7 @@ export const WrappersTab = () => { return ( - + ) } - -const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => ( - -

- There are unsaved changes. Are you sure you want to close the panel? Your changes will be - lost. -

-
-) diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx index 09f21e6e093..cc28ee83d64 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx @@ -1,11 +1,6 @@ import { PostgresPolicy } from '@supabase/postgres-meta' import { useParams } from 'common' -import { isEmpty } from 'lodash' -import { parseAsString, useQueryState } from 'nuqs' -import { useMemo, useState } from 'react' -import { toast } from 'sonner' - -import PolicyEditorModal from 'components/interfaces/Auth/Policies/PolicyEditorModal' +import { PolicyEditorModal } from 'components/interfaces/Auth/Policies/PolicyEditorModal' import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' import { useDatabasePolicyCreateMutation } from 'data/database-policies/database-policy-create-mutation' import { useDatabasePolicyDeleteMutation } from 'data/database-policies/database-policy-delete-mutation' @@ -13,8 +8,12 @@ import { useDatabasePolicyUpdateMutation } from 'data/database-policies/database import { usePaginatedBucketsQuery } from 'data/storage/buckets-query' import { useDebouncedValue } from 'hooks/misc/useDebouncedValue' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { isEmpty } from 'lodash' +import { parseAsString, useQueryState } from 'nuqs' +import { useMemo, useState } from 'react' +import { toast } from 'sonner' import { GenericSkeletonLoader } from 'ui-patterns' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, @@ -24,10 +23,11 @@ import { PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' + import { formatPoliciesForStorage, UNGROUPED_POLICY_SYMBOL } from '../Storage.utils' import { StoragePoliciesBucketRow } from './StoragePoliciesBucketRow' import { BucketsPolicies, type SelectBucketPolicyForAction } from './StoragePoliciesBucketsSection' -import StoragePoliciesEditPolicyModal from './StoragePoliciesEditPolicyModal' +import { StoragePoliciesEditPolicyModal } from './StoragePoliciesEditPolicyModal' export const StoragePolicies = () => { const { ref: projectRef } = useParams() diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx index c3f2492d349..43a1ad0b6a2 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx @@ -1,14 +1,14 @@ -import { noop, pull } from 'lodash' -import { useEffect, useState } from 'react' -import { toast } from 'sonner' - import { POLICY_MODAL_VIEWS } from 'components/interfaces/Auth/Policies/Policies.constants' import PolicySelection from 'components/interfaces/Auth/Policies/PolicySelection' import PolicyTemplates from 'components/interfaces/Auth/Policies/PolicyTemplates' import { DocsButton } from 'components/ui/DocsButton' import { DOCS_URL } from 'lib/constants' +import { noop, pull } from 'lodash' import { ChevronLeft } from 'lucide-react' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' import { Modal } from 'ui' + import { applyBucketIdToTemplateDefinition, createPayloadsForAddPolicy, @@ -26,7 +26,7 @@ const newPolicyTemplate: any = { allowedOperations: [], } -const StoragePoliciesEditPolicyModal = ({ +export const StoragePoliciesEditPolicyModal = ({ visible = false, bucketName = '', onSelectCancel = () => {}, @@ -260,5 +260,3 @@ const StoragePoliciesEditPolicyModal = ({ ) } - -export default StoragePoliciesEditPolicyModal diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index 505845098da..bcef46ef901 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -29,7 +29,7 @@ import { getTables } from 'data/tables/tables-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { isValidExperimentVariant } from 'hooks/misc/useTableCreateGeneratePolicies' -import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' +import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' import { usePHFlag } from 'hooks/ui/useFlag' import { useUrlState } from 'hooks/ui/useUrlState' import { useTrack } from 'lib/telemetry/track' @@ -41,7 +41,7 @@ import { useTableEditorStateSnapshot, type TableEditorState } from 'state/table- import { createTabId, useTabsStateSnapshot } from 'state/tabs' import type { Dictionary } from 'types' import { SonnerProgress } from 'ui' -import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' +import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { ColumnEditor } from './ColumnEditor/ColumnEditor' import type { ForeignKey } from './ForeignKeySelector/ForeignKeySelector.types' @@ -229,7 +229,7 @@ export const SidePanelEditor = ({ } ) - const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ + const { confirmOnClose, modalProps } = useConfirmOnClose({ checkIsDirty: () => isEdited, onClose: () => { setIsEdited(false) @@ -1011,22 +1011,7 @@ export const SidePanelEditor = ({ visible={snap.sidePanel?.type === 'operation-queue'} closePanel={snap.closeSidePanel} /> - + ) } - -const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => ( - -

- There are unsaved changes. Are you sure you want to close the panel? Your changes will be - lost. -

-
-) diff --git a/apps/studio/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog.tsx b/apps/studio/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog.tsx index 26413728999..b7d920062ed 100644 --- a/apps/studio/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog.tsx +++ b/apps/studio/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog.tsx @@ -19,16 +19,18 @@ interface DiscardChangesConfirmationDialogProps extends ConfirmOnCloseModalProps description?: ReactNode confirmLabel?: ReactNode cancelLabel?: ReactNode + size?: React.ComponentProps['size'] } export const DiscardChangesConfirmationDialog = ({ visible, onClose, onCancel, - title = 'Discard changes?', - description = 'Are you sure you want to discard your changes? Any unsaved changes will be lost.', + title = 'Unsaved changes', + description = 'You have unsaved changes. Are you sure you want to discard them?', confirmLabel = 'Discard changes', cancelLabel = 'Keep editing', + size = 'tiny', }: DiscardChangesConfirmationDialogProps) => { const isConfirmingRef = useRef(false) @@ -59,7 +61,7 @@ export const DiscardChangesConfirmationDialog = ({ return ( - + {title} {description !== undefined && description !== null && (