Files
supabase/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx
Danny WhiteandJoshen Lim 8a5ad58f81 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>
2026-03-06 03:17:35 +00:00

109 lines
3.6 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
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 } from 'hooks/ui/useConfirmOnClose'
import { HTMLProps, ReactNode, useCallback, useState } from 'react'
import { Sheet, SheetContent } from 'ui'
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { CreateWrapperSheet } from './CreateWrapperSheet'
import { WRAPPERS } from './Wrappers.constants'
import { wrapperMetaComparator } from './Wrappers.utils'
import { WrapperTable } from './WrapperTable'
export const WrappersTab = () => {
const { id } = useParams()
const { data: project } = useSelectedProjectQuery()
const [createWrapperShown, setCreateWrapperShown] = useState(false)
const { can: canCreateWrapper } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'wrappers'
)
const { data } = useFDWsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const wrappers = data ?? []
const wrapperMeta = WRAPPERS.find((w) => w.name === id)
// this contains a collection of all wrapper instances for the wrapper type
const createdWrappers = wrapperMeta
? wrappers.filter((w) => wrapperMetaComparator(wrapperMeta, w))
: []
const [isDirty, setIsDirty] = useState(false)
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
checkIsDirty: useCallback(() => isDirty, [isDirty]),
onClose: useCallback(() => {
setCreateWrapperShown(false)
setIsDirty(false)
}, []),
})
const Container = useCallback(
({ ...props }: { children: ReactNode } & HTMLProps<HTMLDivElement>) => (
<div className="w-full mx-10 py-10 ">
{props.children}
<Sheet open={!!createWrapperShown} onOpenChange={handleOpenChange}>
<SheetContent size="lg" tabIndex={undefined}>
{wrapperMeta && (
<CreateWrapperSheet
wrapperMeta={wrapperMeta}
onDirty={setIsDirty}
onClose={() => setCreateWrapperShown(false)}
onCloseWithConfirmation={confirmOnClose}
/>
)}
</SheetContent>
</Sheet>
</div>
),
[createWrapperShown, handleOpenChange, wrapperMeta, confirmOnClose]
)
if (!wrapperMeta) {
return <div>Missing integration.</div>
}
if (createdWrappers.length === 0) {
return (
<Container>
<div className=" w-full h-48 max-w-4xl">
<div className="border rounded-lg h-full flex flex-col gap-y-2 items-center justify-center">
<p className="text-sm text-foreground-light">
No {wrapperMeta.label} wrappers have been installed
</p>
<ButtonTooltip
type="default"
onClick={() => setCreateWrapperShown(true)}
disabled={!canCreateWrapper}
tooltip={{
content: {
text: !canCreateWrapper
? 'You need additional permissions to create a foreign data wrapper'
: undefined,
},
}}
>
Add new wrapper
</ButtonTooltip>
</div>
</div>
</Container>
)
}
return (
<Container>
<WrapperTable />
<DiscardChangesConfirmationDialog {...modalProps} />
</Container>
)
}