chore(studio): replace CloseConfirmationModal with DiscardChangesConfirmationDialog (#43430)

## 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 |
| --- | --- |
| <img width="987" height="569" alt="Mercor Apexroles Foo
Supabase-9A40EC7C-F335-4B26-B567-450FC0845463"
src="https://github.com/user-attachments/assets/363bed82-34d2-4cc8-9164-6d18cfdbdbbc"
/> | <img width="987" height="569" alt="Mercor Apexroles Foo
Supabase-F427F1FA-DECC-4194-B663-A9E5A6F285A1"
src="https://github.com/user-attachments/assets/d49fafdc-a5c2-46df-9b67-ec42bacbe716"
/> |

## 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 <joshenlimek@gmail.com>
This commit is contained in:
Danny WhiteandJoshen Lim authored and GitHub committed 2026-03-06 03:17:35 +00:00
1 parent a832d62785
commit 8a5ad58f81
25 files changed
+718 -721

No files matched your search

+11
View File
@@ -1292,6 +1292,17 @@ export const Index: Record<string, any> = {
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",
@@ -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`.
<ComponentPreview name="confirmation-modal-demo" peekCode wide />
@@ -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
<ComponentPreview name="alert-dialog-demo" />
#### 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.
<ComponentPreview name="sheet-demo" />
## 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,
})
<Sheet open={visible} onOpenChange={handleOpenChange}>
...
<Button type="default" onClick={confirmOnClose}>
Cancel
</Button>
...
<DiscardChangesConfirmationDialog {...modalProps} />
</Sheet>
```
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`).
<ComponentPreview name="sheet-confirm-on-close-demo" />
@@ -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 (
<AlertDialog open={visible} onOpenChange={handleOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Discard changes?</AlertDialogTitle>
<AlertDialogDescription>
Any unsaved changes to this endpoint will be lost.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep editing</AlertDialogCancel>
<AlertDialogAction variant="danger" onClick={handleConfirm}>
Discard changes
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
export default function SheetConfirmOnCloseDemo() {
const [open, setOpen] = useState(false)
const [savedValues, setSavedValues] = useState<EndpointValues>(defaultValues)
const [draftValues, setDraftValues] = useState<EndpointValues>(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 (
<>
<Button type="default" onClick={openSheet}>
Open endpoint sheet
</Button>
<Sheet open={open} onOpenChange={handleOpenChange}>
<SheetContent className="flex flex-col gap-0">
<SheetHeader>
<SheetTitle>Edit endpoint</SheetTitle>
</SheetHeader>
<Separator />
<SheetSection className="space-y-4">
<div className="space-y-2">
<Label htmlFor="endpoint-url">Endpoint URL</Label>
<Input
id="endpoint-url"
value={draftValues.endpointUrl}
placeholder="https://api.example.com/webhooks/supabase"
onChange={(event) =>
setDraftValues((current) => ({
...current,
endpointUrl: event.target.value,
}))
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="secret-header">Secret header</Label>
<Input
id="secret-header"
value={draftValues.secretHeader}
placeholder="Bearer top-secret-value"
onChange={(event) =>
setDraftValues((current) => ({
...current,
secretHeader: event.target.value,
}))
}
/>
</div>
</SheetSection>
<Separator />
<SheetFooter>
<Button type="default" onClick={confirmOnClose}>
Cancel
</Button>
<Button onClick={saveChanges} disabled={!isDirty}>
Save changes
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
<DiscardChangesAlertDialog {...modalProps} />
</>
)
}
+6
View File
@@ -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',
@@ -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)
}
@@ -14,7 +14,7 @@ interface PolicyEditorProps {
onReviewPolicy: () => void
}
const PolicyEditor = ({
export const PolicyEditor = ({
isNewPolicy = true,
policyFormFields = {},
onUpdatePolicyFormFields = () => {},
@@ -75,5 +75,3 @@ const PolicyEditor = ({
</div>
)
}
export default PolicyEditor
@@ -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<boolean>
@@ -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<PolicyFormField>(
initializedPolicyFormFields
)
const [policyStatementForReview, setPolicyStatementForReview] = useState<any>('')
const [policyStatementForReview, setPolicyStatementForReview] = useState<PolicyForReview>()
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}
>
<div>
<CloseConfirmationModal {...closeConfirmationModalProps} />
<DiscardChangesConfirmationDialog {...modalProps} />
{view === POLICY_MODAL_VIEWS.SELECTION ? (
<PolicySelection
description="Write rules with PostgreSQL's policies to fit your unique business needs."
@@ -221,7 +223,7 @@ const PolicyEditorModal = ({
templatesNote="* References a specific column in the table"
onUseTemplate={onUseTemplate}
/>
) : view === POLICY_MODAL_VIEWS.REVIEW ? (
) : view === POLICY_MODAL_VIEWS.REVIEW && !!policyStatementForReview ? (
<PolicyReview
policy={policyStatementForReview}
onSelectBack={onViewEditor}
@@ -232,20 +234,3 @@ const PolicyEditorModal = ({
</Modal>
)
}
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
<ConfirmationModal
visible={visible}
title="Discard changes"
confirmLabel="Discard"
onCancel={onCancel}
onConfirm={onClose}
>
<p className="text-sm text-foreground-light">
There are unsaved changes. Are you sure you want to close the editor? Your changes will be
lost.
</p>
</ConfirmationModal>
)
export default PolicyEditorModal
@@ -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 ({
<>
<Form_Shadcn_ {...form}>
<form id={FORM_ID} onSubmit={form.handleSubmit(onSubmit)}>
<Sheet open={visible} onOpenChange={confirmOnClose}>
<Sheet open={visible} onOpenChange={handleOpenChange}>
<SheetContent
showClose={false}
size={showTools ? 'lg' : 'default'}
@@ -573,24 +573,12 @@ export const PolicyEditorPanel = memo(function ({
</form>
</Form_Shadcn_>
<CloseConfirmationModal {...closeConfirmationModalProps} />
<DiscardChangesConfirmationDialog
{...modalProps}
description="Are you sure you want to close the editor? Any unsaved changes on your policy and conversations with the Assistant will be lost."
/>
</>
)
})
PolicyEditorPanel.displayName = 'PolicyEditorPanel'
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
<ConfirmationModal
visible={visible}
title="Discard changes"
confirmLabel="Discard"
onCancel={onCancel}
onConfirm={onClose}
>
<p className="text-sm text-foreground-light">
Are you sure you want to close the editor? Any unsaved changes on your policy and
conversations with the Assistant will be lost.
</p>
</ConfirmationModal>
)
@@ -64,5 +64,3 @@ export const PolicyReview = ({
</>
)
}
export default PolicyReview
@@ -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 (
<Sheet open={visible} onOpenChange={confirmOnClose}>
<Sheet open={visible} onOpenChange={handleOpenChange}>
<SheetContent
showClose={false}
size={'default'}
@@ -402,27 +402,12 @@ export const CreateFunction = ({
</Button>
</SheetFooter>
</div>
<CloseConfirmationModal {...closeConfirmationModalProps} />
<DiscardChangesConfirmationDialog {...modalProps} />
</SheetContent>
</Sheet>
)
}
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
<ConfirmationModal
visible={visible}
title="Discard changes"
confirmLabel="Discard"
onCancel={onCancel}
onConfirm={onClose}
>
<p className="text-sm text-foreground-light">
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
lost.
</p>
</ConfirmationModal>
)
interface FormFieldConfigParamsProps {
readonly?: boolean
}
@@ -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 = () => {
</form>
</Form_Shadcn_>
</SidePanel>
<CloseConfirmationModal {...closeConfirmationModalProps} />
<DiscardChangesConfirmationDialog {...modalProps} />
</>
)
}
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
<ConfirmationModal
visible={visible}
title="Discard changes"
confirmLabel="Discard"
onCancel={onCancel}
onConfirm={onClose}
>
<p className="text-sm text-foreground-light">
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
lost.
</p>
</ConfirmationModal>
)
@@ -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 (
<>
<Sheet open={open} onOpenChange={confirmOnClose}>
<Sheet open={open} onOpenChange={handleOpenChange}>
<SheetContent size="lg" className="flex flex-col gap-0">
<SheetHeader>
<SheetTitle>
@@ -481,7 +481,7 @@ export const TriggerSheet = ({
</Button>
</SheetFooter>
<CloseConfirmationModal {...closeConfirmationModalProps} />
<DiscardChangesConfirmationDialog {...modalProps} />
</SheetContent>
</Sheet>
@@ -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) => (
<ConfirmationModal
visible={visible}
title="Discard changes"
confirmLabel="Discard"
onCancel={onCancel}
onConfirm={onClose}
>
<p className="text-sm text-foreground-light">
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
lost.
</p>
</ConfirmationModal>
)
@@ -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<FormSchemaType>({
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 (
<Sheet open={visible} onOpenChange={handleOpenChange}>
<SheetContent
showClose={false}
size={'default'}
className={'!min-w-screen lg:!min-w-[600px] flex flex-col'}
>
<Header />
<Separator />
<FormBody form={form} onSubmit={onSubmit} />
<SheetContent size="default" className={'!min-w-screen lg:!min-w-[600px] flex flex-col'}>
<SheetHeader className="py-3 flex flex-row gap-3 items-center">
<SheetTitle>Edit secret</SheetTitle>
</SheetHeader>
<SheetSection className="h-full">
<Form_Shadcn_ {...form}>
<form
id={FORM_ID}
className="flex flex-col gap-y-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField_Shadcn_
control={form.control}
name="name"
render={({ field }) => (
<FormItemLayout label="Name" layout="horizontal">
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
readOnly
className="!text-foreground-light cursor-not-allowed"
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
control={form.control}
name="value"
render={({ field }) => (
<FormItemLayout
label="Value"
layout="horizontal"
description="Secrets can’t be retrieved once saved. Enter a new value to overwrite the existing value."
>
<FormControl_Shadcn_>
<Input
{...field}
type={showSecretValue ? 'text' : 'password'}
placeholder="my-secret-value"
data-1p-ignore
data-lpignore="true"
data-form-type="other"
data-bwignore
actions={
<div className="mr-1">
<Button
type="text"
className="px-1"
icon={showSecretValue ? <EyeOff /> : <Eye />}
onClick={() => setShowSecretValue(!showSecretValue)}
/>
</div>
}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</form>
</Form_Shadcn_>
</SheetSection>
<SheetFooter>
<Button disabled={isUpdating} type="default" onClick={confirmOnClose}>
Cancel
@@ -103,116 +152,8 @@ export function EditSecretSheet({ secret, visible, onClose }: EditSecretSheetPro
</Button>
</SheetFooter>
</SheetContent>
<DiscardChangesConfirmationDialog {...closeConfirmationModalProps} />
<DiscardChangesConfirmationDialog {...modalProps} />
</Sheet>
)
}
const Header = (): ReactNode => {
return (
<SheetHeader className="py-3 flex flex-row gap-3 items-center border-b-0">
<SheetClose
className={cn(
'text-muted hover:text ring-offset-background hover:opacity-100',
'focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'disabled:pointer-events-none data-[state=open]:bg-secondary',
'transition'
)}
>
<X className="h-3 w-3" />
<span className="sr-only">Close</span>
</SheetClose>
<SheetTitle>Edit secret</SheetTitle>
</SheetHeader>
)
}
type FormBodyProps = {
form: UseFormReturn<FormSchemaType>
onSubmit: SubmitHandler<FormSchemaType>
}
const FormBody = ({ form, onSubmit }: FormBodyProps): ReactNode => {
return (
<Form_Shadcn_ {...form}>
<form id={FORM_ID} className="flex-grow overflow-auto" onSubmit={form.handleSubmit(onSubmit)}>
<SheetSection>
<NameField form={form} />
</SheetSection>
<Separator />
<SheetSection className="space-y-4">
<SecretField form={form} />
</SheetSection>
<Separator />
</form>
</Form_Shadcn_>
)
}
type NameFieldProps = {
form: UseFormReturn<FormSchemaType>
}
const NameField = ({ form }: NameFieldProps): ReactNode => {
return (
<FormField_Shadcn_
control={form.control}
name="name"
render={({ field }) => (
<FormItemLayout label="Name" layout="horizontal">
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
readOnly
className="!text-foreground-light cursor-not-allowed"
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
)
}
type SecretFieldProps = {
form: UseFormReturn<FormSchemaType>
}
const SecretField = ({ form }: SecretFieldProps): ReactNode => {
const [showSecretValue, setShowSecretValue] = useState(false)
return (
<FormField_Shadcn_
control={form.control}
name="value"
render={({ field }) => (
<FormItemLayout
label="Value"
layout="horizontal"
description="Secrets can’t be retrieved once saved. Enter a new value to overwrite the existing value."
>
<FormControl_Shadcn_>
<Input
{...field}
type={showSecretValue ? 'text' : 'password'}
placeholder="my-secret-value"
data-1p-ignore
data-lpignore="true"
data-form-type="other"
data-bwignore
actions={
<div className="mr-1">
<Button
type="text"
className="px-1"
icon={showSecretValue ? <EyeOff /> : <Eye />}
onClick={() => setShowSecretValue(!showSecretValue)}
/>
</div>
}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
)
}
@@ -18,13 +18,15 @@ import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
RadioGroupStacked,
RadioGroupStackedItem,
Separator,
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetSection,
@@ -43,18 +45,19 @@ import { HttpRequestSection } from '../HttpRequestSection'
import { SqlFunctionSection } from '../SqlFunctionSection'
import { SqlSnippetSection } from '../SqlSnippetSection'
import {
FormSchema,
type CreateCronJobForm,
type CronJobType,
FormSchema,
} from './CreateCronJobSheet.constants'
import { CronJobScheduleSection } from './CronJobScheduleSection'
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
import { isGreaterThanOrEqual } from '@/lib/semver'
interface CreateCronJobSheetProps {
open: boolean
selectedCronJob?: Pick<CronJob, 'jobname' | 'schedule' | 'active' | 'command'>
supportsSeconds: boolean
onDirty: (isDirty: boolean) => void
onClose: () => void
onCloseWithConfirmation: () => void
}
const FORM_ID = 'create-cron-job-sidepanel'
@@ -83,13 +86,7 @@ const buildCommand = (values: CronJobType) => {
return command
}
export const CreateCronJobSheet = ({
selectedCronJob,
supportsSeconds,
onDirty,
onClose,
onCloseWithConfirmation: confirmOnClose,
}: CreateCronJobSheetProps) => {
export const CreateCronJobSheet = ({ open, selectedCronJob, onClose }: CreateCronJobSheetProps) => {
const { childId } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: org } = useSelectedOrganizationQuery()
@@ -100,13 +97,18 @@ export const CreateCronJobSheet = ({
const isEditing = !!selectedCronJob?.jobname
const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false)
const { data } = useDatabaseExtensionsQuery({
const { data = [] } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const pgNetExtension = (data ?? []).find((ext) => ext.name === 'pg_net')
const pgNetExtension = data.find((ext) => ext.name === 'pg_net')
const pgNetExtensionInstalled = pgNetExtension?.installed_version != undefined
const pgCronExtension = data.find((ext) => ext.name === 'pg_cron')
const supportsSeconds = pgCronExtension?.installed_version
? isGreaterThanOrEqual(pgCronExtension.installed_version, '1.5')
: false
const { mutate: sendEvent } = useSendEventMutation()
const { mutate: upsertCronJob, isPending: isUpserting } = useDatabaseCronJobCreateMutation()
const isLoading = isLoadingGetCronJob || isUpserting
@@ -118,25 +120,18 @@ export const CreateCronJobSheet = ({
const cronJobValues = parseCronJobCommand(selectedCronJob?.command || '', project?.ref!)
const defaultValues = {
name: selectedCronJob?.jobname || '',
schedule: selectedCronJob?.schedule || '*/5 * * * *',
supportsSeconds,
values: cronJobValues,
}
const form = useForm<CreateCronJobForm>({
resolver: zodResolver(FormSchema),
defaultValues: {
name: selectedCronJob?.jobname || '',
schedule: selectedCronJob?.schedule || '*/5 * * * *',
supportsSeconds,
values: cronJobValues,
},
defaultValues,
})
useEffect(() => {
const subscription = form.watch(() => {
const isDirty = form.formState.isDirty
onDirty(isDirty)
})
return () => subscription.unsubscribe()
}, [form, onDirty])
const [
cronType,
endpoint,
@@ -162,36 +157,10 @@ export const CreateCronJobSheet = ({
],
})
// update the snippet field when the user changes the any values in the form
useEffect(() => {
const command = buildCommand({
type: cronType,
method,
edgeFunctionName,
timeoutMs,
httpHeaders,
httpBody,
functionName,
schema,
endpoint,
snippet: '',
})
if (command) {
form.setValue('values.snippet', command)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
edgeFunctionName,
endpoint,
method,
// for some reason, the httpHeaders are not memoized and cause the useEffect to trigger even when the value is the same
JSON.stringify(httpHeaders),
httpBody,
timeoutMs,
schema,
functionName,
form,
])
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
checkIsDirty: () => form.formState.isDirty,
onClose: () => onClose(),
})
const onSubmit: SubmitHandler<CreateCronJobForm> = async ({ name, schedule, values }) => {
if (!project) return console.error('Project is required')
@@ -272,177 +241,223 @@ export const CreateCronJobSheet = ({
setIsLoadingGetCronJob(false)
}
// update the snippet field when the user changes the any values in the form
useEffect(() => {
const command = buildCommand({
type: cronType,
method,
edgeFunctionName,
timeoutMs,
httpHeaders,
httpBody,
functionName,
schema,
endpoint,
snippet: '',
})
if (command) {
form.setValue('values.snippet', command)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
cronType,
edgeFunctionName,
endpoint,
method,
// for some reason, the httpHeaders are not memoized and cause the useEffect to trigger even when the value is the same
JSON.stringify(httpHeaders),
httpBody,
timeoutMs,
schema,
functionName,
form,
])
useEffect(() => {
if (open && !!pgCronExtension) form.reset(defaultValues)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])
return (
<>
<div className="flex flex-col h-full" tabIndex={-1}>
<SheetHeader>
<SheetTitle>
{isEditing ? `Edit ${selectedCronJob.jobname}` : `Create a new cron job`}
</SheetTitle>
</SheetHeader>
<DiscardChangesConfirmationDialog {...modalProps} />
<div className="overflow-auto flex-grow">
<Form_Shadcn_ {...form}>
<form
id={FORM_ID}
className="flex-grow overflow-auto"
onSubmit={form.handleSubmit(onSubmit)}
>
<SheetSection>
<FormField_Shadcn_
control={form.control}
name="name"
render={({ field }) => (
<FormItemLayout label="Name" layout="vertical" className="gap-1 relative">
<FormControl_Shadcn_>
<Input_Shadcn_ {...field} disabled={isEditing} />
</FormControl_Shadcn_>
<span className="text-foreground-lighter text-xs absolute top-0 right-0">
Cron jobs cannot be renamed once created
</span>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator />
<CronJobScheduleSection form={form} supportsSeconds={supportsSeconds} />
<Separator />
<SheetSection>
<FormField_Shadcn_
control={form.control}
name="values.type"
render={({ field }) => (
<FormItemLayout label="Type" layout="vertical" className="gap-1">
<FormControl_Shadcn_>
<RadioGroupStacked
id="function_type"
name="function_type"
value={field.value}
disabled={field.disabled}
onValueChange={(value) => field.onChange(value)}
>
{CRONJOB_DEFINITIONS.map((definition) => (
<RadioGroupStackedItem
key={definition.value}
id={definition.value}
value={definition.value}
disabled={
!pgNetExtensionInstalled &&
(definition.value === 'http_request' ||
definition.value === 'edge_function')
}
label=""
showIndicator={false}
<Sheet open={open} onOpenChange={handleOpenChange}>
<SheetContent size="lg">
<div className="flex flex-col h-full" tabIndex={-1}>
<SheetHeader>
<SheetTitle>
{isEditing ? `Edit ${selectedCronJob.jobname}` : `Create a new cron job`}
</SheetTitle>
</SheetHeader>
<div className="overflow-auto flex-grow">
<Form_Shadcn_ {...form}>
<form
id={FORM_ID}
className="flex-grow overflow-auto"
onSubmit={form.handleSubmit(onSubmit)}
>
<SheetSection>
<FormField_Shadcn_
control={form.control}
name="name"
render={({ field }) => (
<FormItemLayout label="Name" layout="vertical" className="gap-1 relative">
<FormControl_Shadcn_>
<Input_Shadcn_ {...field} disabled={isEditing} />
</FormControl_Shadcn_>
<span className="text-foreground-lighter text-xs absolute top-0 right-0">
Cron jobs cannot be renamed once created
</span>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator />
<CronJobScheduleSection form={form} supportsSeconds={supportsSeconds} />
<Separator />
<SheetSection>
<FormField_Shadcn_
control={form.control}
name="values.type"
render={({ field }) => (
<FormItemLayout label="Type" layout="vertical" className="gap-1">
<FormControl_Shadcn_>
<RadioGroupStacked
id="function_type"
name="function_type"
value={field.value}
disabled={field.disabled}
onValueChange={(value) => field.onChange(value)}
>
<div className="flex items-center gap-x-5">
<div className="text-foreground">{definition.icon}</div>
<div className="flex flex-col">
<div className="flex gap-x-2">
<p className="text-foreground">{definition.label}</p>
{CRONJOB_DEFINITIONS.map((definition) => (
<RadioGroupStackedItem
key={definition.value}
id={definition.value}
value={definition.value}
disabled={
!pgNetExtensionInstalled &&
(definition.value === 'http_request' ||
definition.value === 'edge_function')
}
label=""
showIndicator={false}
>
<div className="flex items-center gap-x-5">
<div className="text-foreground">{definition.icon}</div>
<div className="flex flex-col">
<div className="flex gap-x-2">
<p className="text-foreground">{definition.label}</p>
</div>
<p className="text-foreground-light">
{definition.description}
</p>
</div>
</div>
<p className="text-foreground-light">{definition.description}</p>
</div>
</div>
{!pgNetExtensionInstalled &&
(definition.value === 'http_request' ||
definition.value === 'edge_function') ? (
<div className="w-full flex gap-x-2 pl-11 py-2 items-center">
<WarningIcon />
<span className="text-xs">
<code>pg_net</code> needs to be installed to use this type
</span>
</div>
) : null}
</RadioGroupStackedItem>
))}
</RadioGroupStacked>
</FormControl_Shadcn_>
</FormItemLayout>
{!pgNetExtensionInstalled &&
(definition.value === 'http_request' ||
definition.value === 'edge_function') ? (
<div className="w-full flex gap-x-2 pl-11 py-2 items-center">
<WarningIcon />
<span className="text-xs">
<code>pg_net</code> needs to be installed to use this type
</span>
</div>
) : null}
</RadioGroupStackedItem>
))}
</RadioGroupStacked>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
{!pgNetExtensionInstalled && (
<Admonition
type="note"
// @ts-ignore
title={
<span>
Enable <code className="text-code-inline w-min">pg_net</code> for HTTP
requests or Edge Functions
</span>
}
description={
<div className="flex flex-col gap-y-2">
<span>
This will allow you to send HTTP requests or trigger an edge function
within your cron jobs
</span>
<ButtonTooltip
type="default"
className="w-min"
disabled={!canToggleExtensions}
onClick={() => setShowEnableExtensionModal(true)}
tooltip={{
content: {
side: 'bottom',
text: !canToggleExtensions
? 'You need additional permissions to enable database extensions'
: undefined,
},
}}
>
Install pg_net extension
</ButtonTooltip>
</div>
}
/>
)}
</SheetSection>
<Separator />
{cronType === 'http_request' && (
<>
<HttpRequestSection form={form} />
<Separator />
<HTTPHeaderFieldsSection variant={cronType} />
<Separator />
<HttpBodyFieldSection form={form} />
</>
)}
/>
{!pgNetExtensionInstalled && (
<Admonition
type="note"
// @ts-ignore
title={
<span>
Enable <code className="text-code-inline w-min">pg_net</code> for HTTP
requests or Edge Functions
</span>
}
description={
<div className="flex flex-col gap-y-2">
<span>
This will allow you to send HTTP requests or trigger an edge function
within your cron jobs
</span>
<ButtonTooltip
type="default"
className="w-min"
disabled={!canToggleExtensions}
onClick={() => setShowEnableExtensionModal(true)}
tooltip={{
content: {
side: 'bottom',
text: !canToggleExtensions
? 'You need additional permissions to enable database extensions'
: undefined,
},
}}
>
Install pg_net extension
</ButtonTooltip>
</div>
}
/>
)}
</SheetSection>
<Separator />
{cronType === 'http_request' && (
<>
<HttpRequestSection form={form} />
<Separator />
<HTTPHeaderFieldsSection variant={cronType} />
<Separator />
<HttpBodyFieldSection form={form} />
</>
)}
{cronType === 'edge_function' && (
<>
<EdgeFunctionSection form={form} />
<Separator />
<HTTPHeaderFieldsSection variant={cronType} />
<Separator />
<HttpBodyFieldSection form={form} />
</>
)}
{cronType === 'sql_function' && <SqlFunctionSection form={form} />}
{cronType === 'sql_snippet' && <SqlSnippetSection form={form} />}
</form>
</Form_Shadcn_>
</div>
<SheetFooter>
<Button
size="tiny"
type="default"
htmlType="button"
onClick={confirmOnClose}
disabled={isLoading}
>
Cancel
</Button>
<Button
size="tiny"
type="primary"
form={FORM_ID}
htmlType="submit"
disabled={isLoading}
loading={isLoading}
>
{isEditing ? `Save cron job` : 'Create cron job'}
</Button>
</SheetFooter>
</div>
{cronType === 'edge_function' && (
<>
<EdgeFunctionSection form={form} />
<Separator />
<HTTPHeaderFieldsSection variant={cronType} />
<Separator />
<HttpBodyFieldSection form={form} />
</>
)}
{cronType === 'sql_function' && <SqlFunctionSection form={form} />}
{cronType === 'sql_snippet' && <SqlSnippetSection form={form} />}
</form>
</Form_Shadcn_>
</div>
<SheetFooter>
<Button
size="tiny"
type="default"
htmlType="button"
onClick={confirmOnClose}
disabled={isLoading}
>
Cancel
</Button>
<Button
size="tiny"
type="primary"
form={FORM_ID}
htmlType="submit"
disabled={isLoading}
loading={isLoading}
>
{isEditing ? `Save cron job` : 'Create cron job'}
</Button>
</SheetFooter>
</div>
</SheetContent>
</Sheet>
{pgNetExtension && (
<EnableExtensionModal
visible={showEnableExtensionModal}
@@ -3,7 +3,6 @@ import { toString as CronToString } from 'cronstrue'
import { useCronJobQuery } from 'data/database-cron-jobs/database-cron-job-query'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { type ConfirmOnCloseModalProps, useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
import { Edit3, List } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
@@ -15,15 +14,12 @@ import {
BreadcrumbPage_Shadcn_ as BreadcrumbPage,
BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator,
Button,
cn,
CodeBlock,
Sheet,
SheetContent,
Tooltip,
TooltipContent,
TooltipTrigger,
cn,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import {
PageHeader,
PageHeaderAside,
@@ -64,15 +60,6 @@ export const CronJobPage = () => {
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 = () => {
<PreviousRunsTab />
<Sheet open={isEditSheetOpen} onOpenChange={setIsEditSheetOpen}>
<SheetContent size="lg">
{job && (
<CreateCronJobSheet
selectedCronJob={{
jobname: job.jobname,
schedule: job.schedule,
active: job.active,
command: job.command,
}}
supportsSeconds={true}
onDirty={setIsDirty}
onClose={() => setIsEditSheetOpen(false)}
onCloseWithConfirmation={confirmOnClose}
/>
)}
</SheetContent>
</Sheet>
<CloseConfirmationModal {...closeConfirmationModalProps} />
{job && (
<CreateCronJobSheet
open={isEditSheetOpen}
selectedCronJob={job}
onClose={() => setIsEditSheetOpen(false)}
/>
)}
</>
)
}
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
<ConfirmationModal
visible={visible}
title="Discard changes"
confirmLabel="Discard"
onCancel={onCancel}
onConfirm={onClose}
>
<p className="text-sm text-foreground-light">
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
lost.
</p>
</ConfirmationModal>
)
@@ -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 = () => {
<DeleteCronJob />
<Sheet open={!!createCronJobSheetShown || !!cronJobForEditing} onOpenChange={confirmOnClose}>
<SheetContent size="default" tabIndex={undefined}>
<CreateCronJobSheet
selectedCronJob={cronJobForEditing ?? EMPTY_CRON_JOB}
supportsSeconds={supportsSeconds}
onDirty={setIsDirty}
onClose={onClose}
onCloseWithConfirmation={confirmOnClose}
/>
</SheetContent>
</Sheet>
<CloseConfirmationModal {...closeConfirmationModalProps} />
<CreateCronJobSheet
open={!!createCronJobSheetShown || !!cronJobForEditing}
selectedCronJob={cronJobForEditing ?? EMPTY_CRON_JOB}
onClose={onClose}
/>
</>
)
}
@@ -228,19 +211,3 @@ const CronJobsFooter = ({ count }: CronJobsFooterProps) => (
)}
</div>
)
// Confirmation modal for unsaved changes
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
<ConfirmationModal
visible={visible}
title="Discard changes"
confirmLabel="Discard"
onCancel={onCancel}
onConfirm={onClose}
>
<p className="text-sm text-foreground-light">
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
lost.
</p>
</ConfirmationModal>
)
@@ -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 (
<Sheet open={visible} onOpenChange={confirmOnClose}>
<Sheet open={visible} onOpenChange={handleOpenChange}>
<SheetContent size="default" className="w-[35%]" tabIndex={undefined}>
<div className="flex flex-col h-full" tabIndex={-1}>
<SheetHeader>
@@ -322,23 +322,8 @@ export const CreateQueueSheet = ({ visible, onClose }: CreateQueueSheetProps) =>
</Button>
</SheetFooter>
</div>
<CloseConfirmationModal {...closeConfirmationModalProps} />
<DiscardChangesConfirmationDialog {...modalProps} />
</SheetContent>
</Sheet>
)
}
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
<ConfirmationModal
visible={visible}
title="Discard changes"
confirmLabel="Discard"
onCancel={onCancel}
onConfirm={onClose}
>
<p className="text-sm text-foreground-light">
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
lost.
</p>
</ConfirmationModal>
)
@@ -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 = ({
<p className="text-sm text-foreground-light mt-2">Are you sure you want to continue?</p>
</ConfirmationModal>
<CloseConfirmationModal {...closeConfirmationModalProps} />
<DiscardChangesConfirmationDialog {...modalProps} />
<WrapperTableEditor
visible={isEditingTable}
@@ -412,18 +413,3 @@ export const EditWrapperSheet = ({
</>
)
}
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
<ConfirmationModal
visible={visible}
title="Discard changes"
confirmLabel="Discard"
onCancel={onCancel}
onConfirm={onClose}
>
<p className="text-sm text-foreground-light">
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
lost.
</p>
</ConfirmationModal>
)
@@ -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 = () => {
</div>
<Separator />
<Sheet open={!!createWrapperShown} onOpenChange={confirmOnClose}>
<Sheet open={!!createWrapperShown} onOpenChange={handleOpenChange}>
<SheetContent size="lg" tabIndex={undefined}>
<CreateWrapperSheetComponent
wrapperMeta={wrapperMeta}
@@ -153,22 +153,7 @@ export const WrapperOverviewTab = () => {
/>
</SheetContent>
</Sheet>
<CloseConfirmationModal {...closeConfirmationModalProps} />
<DiscardChangesConfirmationDialog {...modalProps} />
</IntegrationOverviewTab>
)
}
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
<ConfirmationModal
visible={visible}
title="Discard changes"
confirmLabel="Discard"
onCancel={onCancel}
onConfirm={onClose}
>
<p className="text-sm text-foreground-light">
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
lost.
</p>
</ConfirmationModal>
)
@@ -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<HTMLDivElement>) => (
<div className="w-full mx-10 py-10 ">
{props.children}
<Sheet open={!!createWrapperShown} onOpenChange={confirmOnClose}>
<Sheet open={!!createWrapperShown} onOpenChange={handleOpenChange}>
<SheetContent size="lg" tabIndex={undefined}>
{wrapperMeta && (
<CreateWrapperSheet
@@ -64,7 +64,7 @@ export const WrappersTab = () => {
</Sheet>
</div>
),
[createWrapperShown, wrapperMeta, confirmOnClose]
[createWrapperShown, handleOpenChange, wrapperMeta, confirmOnClose]
)
if (!wrapperMeta) {
@@ -102,22 +102,7 @@ export const WrappersTab = () => {
return (
<Container>
<WrapperTable />
<CloseConfirmationModal {...closeConfirmationModalProps} />
<DiscardChangesConfirmationDialog {...modalProps} />
</Container>
)
}
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
<ConfirmationModal
visible={visible}
title="Discard changes"
confirmLabel="Discard"
onCancel={onCancel}
onConfirm={onClose}
>
<p className="text-sm text-foreground-light">
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
lost.
</p>
</ConfirmationModal>
)
@@ -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()
@@ -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 = ({
</Modal>
)
}
export default StoragePoliciesEditPolicyModal
@@ -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}
/>
<CloseConfirmationModal {...closeConfirmationModalProps} />
<DiscardChangesConfirmationDialog {...modalProps} />
</>
)
}
const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (
<ConfirmationModal
visible={visible}
title="Discard changes"
confirmLabel="Discard"
onCancel={onCancel}
onConfirm={onClose}
>
<p className="text-sm text-foreground-light">
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
lost.
</p>
</ConfirmationModal>
)
@@ -19,16 +19,18 @@ interface DiscardChangesConfirmationDialogProps extends ConfirmOnCloseModalProps
description?: ReactNode
confirmLabel?: ReactNode
cancelLabel?: ReactNode
size?: React.ComponentProps<typeof AlertDialogContent>['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 (
<AlertDialog open={visible} onOpenChange={handleOpenChange}>
<AlertDialogContent>
<AlertDialogContent size={size}>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
{description !== undefined && description !== null && (