mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-16 11:51:34 +00:00
multi schemas alert
This commit is contained in:
committed by
Guy Ben-Aharon
parent
2f2828bc36
commit
420733be8e
@@ -18,6 +18,7 @@ import {
|
|||||||
} from '@/components/popover/popover';
|
} from '@/components/popover/popover';
|
||||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export interface SelectBoxOption {
|
export interface SelectBoxOption {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -41,6 +42,8 @@ interface SelectBoxProps {
|
|||||||
showClear?: boolean;
|
showClear?: boolean;
|
||||||
keepOrder?: boolean;
|
keepOrder?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||||
@@ -61,13 +64,27 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
showClear,
|
showClear,
|
||||||
keepOrder,
|
keepOrder,
|
||||||
disabled,
|
disabled,
|
||||||
|
open,
|
||||||
|
onOpenChange: setOpen,
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [isOpen, setIsOpen] = React.useState(open ?? false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsOpen(open ?? false);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const onOpenChange = React.useCallback(
|
||||||
|
(isOpen: boolean) => {
|
||||||
|
setOpen?.(isOpen);
|
||||||
|
setIsOpen(isOpen);
|
||||||
|
},
|
||||||
|
[setOpen]
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelect = React.useCallback(
|
const handleSelect = React.useCallback(
|
||||||
(selectedValue: string) => {
|
(selectedValue: string) => {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
@@ -140,7 +157,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={isOpen} onOpenChange={setIsOpen} modal={true}>
|
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -19,19 +19,25 @@ export function Toaster() {
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
action,
|
action,
|
||||||
|
layout = 'row',
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Toast key={id} {...props}>
|
<Toast key={id} {...props}>
|
||||||
<div className="grid gap-1">
|
<div
|
||||||
|
className={`grid gap-1${layout === 'column' ? ' w-full' : ''}`}
|
||||||
|
>
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
{description && (
|
{description && (
|
||||||
<ToastDescription>
|
<ToastDescription>
|
||||||
{description}
|
{description}
|
||||||
</ToastDescription>
|
</ToastDescription>
|
||||||
)}
|
)}
|
||||||
|
{layout === 'column' ? (
|
||||||
|
<div className="mt-2">{action}</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{action}
|
{layout === 'row' ? action : null}
|
||||||
<ToastClose />
|
<ToastClose />
|
||||||
</Toast>
|
</Toast>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type ToasterToast = ToastProps & {
|
|||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
action?: ToastActionElement;
|
action?: ToastActionElement;
|
||||||
|
layout?: 'row' | 'column';
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionTypes = {
|
const actionTypes = {
|
||||||
|
|||||||
@@ -86,16 +86,23 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
[diagramId, setSchemasFilter]
|
[diagramId, setSchemasFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredSchemas: ChartDBContext['filteredSchemas'] = useMemo(
|
const filteredSchemas: ChartDBContext['filteredSchemas'] = useMemo(() => {
|
||||||
() =>
|
if (schemas.length === 0) {
|
||||||
schemas.length > 0
|
return undefined;
|
||||||
? (schemasFilter[diagramId] ?? [
|
}
|
||||||
|
|
||||||
|
const schemasFilterFromCache =
|
||||||
|
(schemasFilter[diagramId] ?? []).length === 0
|
||||||
|
? undefined // in case of empty filter, skip cache
|
||||||
|
: schemasFilter[diagramId];
|
||||||
|
|
||||||
|
return (
|
||||||
|
schemasFilterFromCache ?? [
|
||||||
schemas.find((s) => s.name === defaultSchemaName)?.id ??
|
schemas.find((s) => s.name === defaultSchemaName)?.id ??
|
||||||
schemas[0]?.id,
|
schemas[0]?.id,
|
||||||
])
|
]
|
||||||
: undefined,
|
|
||||||
[schemasFilter, diagramId, schemas, defaultSchemaName]
|
|
||||||
);
|
);
|
||||||
|
}, [schemasFilter, diagramId, schemas, defaultSchemaName]);
|
||||||
|
|
||||||
const currentDiagram: Diagram = useMemo(
|
const currentDiagram: Diagram = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export interface LayoutContext {
|
|||||||
isSidePanelShowed: boolean;
|
isSidePanelShowed: boolean;
|
||||||
hideSidePanel: () => void;
|
hideSidePanel: () => void;
|
||||||
showSidePanel: () => void;
|
showSidePanel: () => void;
|
||||||
|
|
||||||
|
isSelectSchemaOpen: boolean;
|
||||||
|
openSelectSchema: () => void;
|
||||||
|
closeSelectSchema: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const layoutContext = createContext<LayoutContext>({
|
export const layoutContext = createContext<LayoutContext>({
|
||||||
@@ -35,4 +39,8 @@ export const layoutContext = createContext<LayoutContext>({
|
|||||||
isSidePanelShowed: false,
|
isSidePanelShowed: false,
|
||||||
hideSidePanel: emptyFn,
|
hideSidePanel: emptyFn,
|
||||||
showSidePanel: emptyFn,
|
showSidePanel: emptyFn,
|
||||||
|
|
||||||
|
isSelectSchemaOpen: false,
|
||||||
|
openSelectSchema: emptyFn,
|
||||||
|
closeSelectSchema: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
React.useState<SidebarSection>('tables');
|
React.useState<SidebarSection>('tables');
|
||||||
const [isSidePanelShowed, setIsSidePanelShowed] =
|
const [isSidePanelShowed, setIsSidePanelShowed] =
|
||||||
React.useState<boolean>(isDesktop);
|
React.useState<boolean>(isDesktop);
|
||||||
|
const [isSelectSchemaOpen, setIsSelectSchemaOpen] =
|
||||||
|
React.useState<boolean>(false);
|
||||||
|
|
||||||
const closeAllTablesInSidebar: LayoutContext['closeAllTablesInSidebar'] =
|
const closeAllTablesInSidebar: LayoutContext['closeAllTablesInSidebar'] =
|
||||||
() => setOpenedTableInSidebar('');
|
() => setOpenedTableInSidebar('');
|
||||||
@@ -42,6 +44,12 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setSelectedSidebarSection('relationships');
|
setSelectedSidebarSection('relationships');
|
||||||
setOpenedRelationshipInSidebar(relationshipId);
|
setOpenedRelationshipInSidebar(relationshipId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
|
||||||
|
setIsSelectSchemaOpen(true);
|
||||||
|
|
||||||
|
const closeSelectSchema: LayoutContext['closeSelectSchema'] = () =>
|
||||||
|
setIsSelectSchemaOpen(false);
|
||||||
return (
|
return (
|
||||||
<layoutContext.Provider
|
<layoutContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -56,6 +64,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
isSidePanelShowed,
|
isSidePanelShowed,
|
||||||
hideSidePanel,
|
hideSidePanel,
|
||||||
showSidePanel,
|
showSidePanel,
|
||||||
|
isSelectSchemaOpen,
|
||||||
|
openSelectSchema,
|
||||||
|
closeSelectSchema,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export interface LocalConfigContext {
|
|||||||
|
|
||||||
showCardinality: boolean;
|
showCardinality: boolean;
|
||||||
setShowCardinality: (showCardinality: boolean) => void;
|
setShowCardinality: (showCardinality: boolean) => void;
|
||||||
|
|
||||||
|
hideMultiSchemaNotification: boolean;
|
||||||
|
setHideMultiSchemaNotification: (
|
||||||
|
hideMultiSchemaNotification: boolean
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LocalConfigContext = createContext<LocalConfigContext>({
|
export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||||
@@ -32,4 +37,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
|||||||
|
|
||||||
showCardinality: false,
|
showCardinality: false,
|
||||||
setShowCardinality: emptyFn,
|
setShowCardinality: emptyFn,
|
||||||
|
|
||||||
|
hideMultiSchemaNotification: false,
|
||||||
|
setHideMultiSchemaNotification: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
(localStorage.getItem(showCardinalityKey) || 'false') === 'true'
|
(localStorage.getItem(showCardinalityKey) || 'false') === 'true'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
||||||
|
React.useState<boolean>(
|
||||||
|
(localStorage.getItem('hide_multi_schema_notification') ||
|
||||||
|
'false') === 'true'
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'hide_multi_schema_notification',
|
||||||
|
hideMultiSchemaNotification.toString()
|
||||||
|
);
|
||||||
|
}, [hideMultiSchemaNotification]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(themeKey, theme);
|
localStorage.setItem(themeKey, theme);
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
@@ -59,6 +72,8 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setSchemasFilter,
|
setSchemasFilter,
|
||||||
showCardinality,
|
showCardinality,
|
||||||
setShowCardinality,
|
setShowCardinality,
|
||||||
|
hideMultiSchemaNotification,
|
||||||
|
setHideMultiSchemaNotification,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -62,6 +62,15 @@ export const en = {
|
|||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
multiple_schemas_alert: {
|
||||||
|
title: 'Multiple Schemas',
|
||||||
|
description:
|
||||||
|
'{{schemasCount}} schemas on this diagram, currently showing: {{formattedSchemas}}.',
|
||||||
|
dont_show_again: "Don't show again",
|
||||||
|
change_schema: 'Change',
|
||||||
|
none: 'none',
|
||||||
|
},
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
system: 'System',
|
system: 'System',
|
||||||
light: 'Light',
|
light: 'Light',
|
||||||
|
|||||||
@@ -261,6 +261,15 @@ export const es: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
multiple_schemas_alert: {
|
||||||
|
title: 'Múltiples Esquemas',
|
||||||
|
description:
|
||||||
|
'{{schemasCount}} esquemas en este diagrama, Actualmente mostrando: {{formattedSchemas}}.',
|
||||||
|
dont_show_again: 'No mostrar de nuevo',
|
||||||
|
change_schema: 'Cambiar',
|
||||||
|
none: 'nada',
|
||||||
|
},
|
||||||
|
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Uno a Uno',
|
one_to_one: 'Uno a Uno',
|
||||||
one_to_many: 'Uno a Muchos',
|
one_to_many: 'Uno a Muchos',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { TopNavbar } from './top-navbar/top-navbar';
|
import { TopNavbar } from './top-navbar/top-navbar';
|
||||||
import {
|
import {
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
@@ -16,6 +16,7 @@ import { Toaster } from '@/components/toast/toaster';
|
|||||||
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
||||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
|
import { useToast } from '@/components/toast/use-toast';
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
@@ -25,10 +26,14 @@ import {
|
|||||||
} from '@/components/drawer/drawer';
|
} from '@/components/drawer/drawer';
|
||||||
import { Separator } from '@/components/separator/separator';
|
import { Separator } from '@/components/separator/separator';
|
||||||
import { Diagram } from '@/lib/domain/diagram';
|
import { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import { ToastAction } from '@/components/toast/toast';
|
||||||
|
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export const EditorPage: React.FC = () => {
|
export const EditorPage: React.FC = () => {
|
||||||
const { loadDiagram, currentDiagram } = useChartDB();
|
const { loadDiagram, currentDiagram, schemas, filteredSchemas } =
|
||||||
const { isSidePanelShowed, hideSidePanel } = useLayout();
|
useChartDB();
|
||||||
|
const { isSidePanelShowed, hideSidePanel, openSelectSchema } = useLayout();
|
||||||
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
|
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
|
||||||
const { showLoader, hideLoader } = useFullScreenLoader();
|
const { showLoader, hideLoader } = useFullScreenLoader();
|
||||||
const { openCreateDiagramDialog } = useDialog();
|
const { openCreateDiagramDialog } = useDialog();
|
||||||
@@ -39,6 +44,10 @@ export const EditorPage: React.FC = () => {
|
|||||||
const { isXl } = useBreakpoint('xl');
|
const { isXl } = useBreakpoint('xl');
|
||||||
const { isMd: isDesktop } = useBreakpoint('md');
|
const { isMd: isDesktop } = useBreakpoint('md');
|
||||||
const [initialDiagram, setInitialDiagram] = useState<Diagram | undefined>();
|
const [initialDiagram, setInitialDiagram] = useState<Diagram | undefined>();
|
||||||
|
const { hideMultiSchemaNotification, setHideMultiSchemaNotification } =
|
||||||
|
useLocalConfig();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@@ -90,6 +99,66 @@ export const EditorPage: React.FC = () => {
|
|||||||
updateConfig,
|
updateConfig,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const lastDiagramId = useRef<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastDiagramId.current === currentDiagram.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastDiagramId.current = currentDiagram.id;
|
||||||
|
if (schemas.length > 1 && !hideMultiSchemaNotification) {
|
||||||
|
const formattedSchemas = !filteredSchemas
|
||||||
|
? t('multiple_schemas_alert.none')
|
||||||
|
: filteredSchemas
|
||||||
|
.map((filteredSchema) =>
|
||||||
|
schemas.find((schema) => schema.id === filteredSchema)
|
||||||
|
)
|
||||||
|
.map((schema) => `'${schema?.name}'`)
|
||||||
|
.join(', ');
|
||||||
|
toast({
|
||||||
|
duration: 5500,
|
||||||
|
title: t('multiple_schemas_alert.title'),
|
||||||
|
description: t('multiple_schemas_alert.description', {
|
||||||
|
schemasCount: schemas.length,
|
||||||
|
formattedSchemas,
|
||||||
|
}),
|
||||||
|
variant: 'default',
|
||||||
|
layout: 'column',
|
||||||
|
className:
|
||||||
|
'top-0 right-0 flex fixed md:max-w-[420px] md:top-4 md:right-4',
|
||||||
|
action: (
|
||||||
|
<div className="flex justify-between gap-1">
|
||||||
|
<ToastAction
|
||||||
|
altText="Don't show this notification again"
|
||||||
|
className="flex-nowrap"
|
||||||
|
onClick={() => setHideMultiSchemaNotification(true)}
|
||||||
|
>
|
||||||
|
{t('multiple_schemas_alert.dont_show_again')}
|
||||||
|
</ToastAction>
|
||||||
|
<ToastAction
|
||||||
|
onClick={() => openSelectSchema()}
|
||||||
|
altText="Change the schema"
|
||||||
|
className="border border-pink-600 bg-pink-600 text-white hover:bg-pink-500"
|
||||||
|
>
|
||||||
|
{t('multiple_schemas_alert.change_schema')}
|
||||||
|
</ToastAction>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
schemas,
|
||||||
|
filteredSchemas,
|
||||||
|
toast,
|
||||||
|
currentDiagram.id,
|
||||||
|
diagramId,
|
||||||
|
openSelectSchema,
|
||||||
|
t,
|
||||||
|
hideMultiSchemaNotification,
|
||||||
|
setHideMultiSchemaNotification,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section
|
<section
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -20,7 +20,13 @@ export interface SidePanelProps {}
|
|||||||
export const SidePanel: React.FC<SidePanelProps> = () => {
|
export const SidePanel: React.FC<SidePanelProps> = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { schemas, filterSchemas, filteredSchemas } = useChartDB();
|
const { schemas, filterSchemas, filteredSchemas } = useChartDB();
|
||||||
const { selectSidebarSection, selectedSidebarSection } = useLayout();
|
const {
|
||||||
|
selectSidebarSection,
|
||||||
|
selectedSidebarSection,
|
||||||
|
isSelectSchemaOpen,
|
||||||
|
openSelectSchema,
|
||||||
|
closeSelectSchema,
|
||||||
|
} = useLayout();
|
||||||
|
|
||||||
const schemasOptions: SelectBoxOption[] = useMemo(
|
const schemasOptions: SelectBoxOption[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -34,6 +40,17 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
|
|||||||
[schemas]
|
[schemas]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setIsSelectSchemaOpen = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
if (open) {
|
||||||
|
openSelectSchema();
|
||||||
|
} else {
|
||||||
|
closeSelectSchema();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[openSelectSchema, closeSelectSchema]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="flex h-full flex-col overflow-hidden">
|
<aside className="flex h-full flex-col overflow-hidden">
|
||||||
{schemasOptions.length > 0 && (
|
{schemasOptions.length > 0 && (
|
||||||
@@ -56,6 +73,8 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
|
|||||||
inputPlaceholder={t('side_panel.search_schema')}
|
inputPlaceholder={t('side_panel.search_schema')}
|
||||||
emptyPlaceholder={t('side_panel.no_schemas_found')}
|
emptyPlaceholder={t('side_panel.no_schemas_found')}
|
||||||
multiple
|
multiple
|
||||||
|
open={isSelectSchemaOpen}
|
||||||
|
onOpenChange={setIsSelectSchemaOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user