multi schemas alert

This commit is contained in:
Guy Ben-Aharon
2024-09-19 23:45:22 +03:00
committed by Guy Ben-Aharon
parent 2f2828bc36
commit 420733be8e
12 changed files with 198 additions and 19 deletions

View File

@@ -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(

View File

@@ -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>
); );

View File

@@ -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 = {

View File

@@ -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(
() => ({ () => ({

View File

@@ -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,
}); });

View File

@@ -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}

View File

@@ -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,
}); });

View File

@@ -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}

View File

@@ -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',

View File

@@ -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',

View File

@@ -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

View File

@@ -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>