Compare commits

...

1 Commits

Author SHA1 Message Date
johnnyfish
76f9662b80 feat(open-diagram): add row menu options for open diagram dialog 2025-08-26 10:05:48 +03:00
25 changed files with 264 additions and 10 deletions

View File

@@ -764,6 +764,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
db.db_dependencies.where('diagramId').equals(id).delete(),
db.areas.where('diagramId').equals(id).delete(),
db.db_custom_types.where('diagramId').equals(id).delete(),
db.diagram_filters.where('diagramId').equals(id).delete(),
]);
},
[db]

View File

@@ -0,0 +1,216 @@
import React, { useCallback, useState } from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/dropdown-menu/dropdown-menu';
import { Button } from '@/components/button/button';
import type { Diagram } from '@/lib/domain/diagram';
import {
Copy,
MoreHorizontal,
SquareArrowOutUpRight,
Trash2,
Loader2,
} from 'lucide-react';
import { useStorage } from '@/hooks/use-storage';
import { useAlert } from '@/context/alert-context/alert-context';
import { useTranslation } from 'react-i18next';
import { cloneDiagram } from '@/lib/clone';
import { useParams, useNavigate } from 'react-router-dom';
import { useConfig } from '@/hooks/use-config';
interface DiagramRowActionsMenuProps {
diagram: Diagram;
onOpen: () => void;
refetch: () => void;
onSelectDiagram?: (diagramId: string | undefined) => void;
}
export const DiagramRowActionsMenu: React.FC<DiagramRowActionsMenuProps> = ({
diagram,
onOpen,
refetch,
onSelectDiagram,
}) => {
const { addDiagram, deleteDiagram, listDiagrams, getDiagram } =
useStorage();
const { showAlert } = useAlert();
const { t } = useTranslation();
const { diagramId: currentDiagramId } = useParams<{ diagramId: string }>();
const navigate = useNavigate();
const { updateConfig } = useConfig();
const [isDuplicating, setIsDuplicating] = useState(false);
const handleDuplicateDiagram = useCallback(async () => {
setIsDuplicating(true);
try {
// Load the full diagram with all components
const fullDiagram = await getDiagram(diagram.id, {
includeTables: true,
includeRelationships: true,
includeAreas: true,
includeDependencies: true,
includeCustomTypes: true,
});
if (!fullDiagram) {
console.error('Failed to load diagram for duplication');
setIsDuplicating(false);
return;
}
const { diagram: clonedDiagram } = cloneDiagram(fullDiagram);
// Generate a unique name for the duplicated diagram
const diagrams = await listDiagrams();
const existingNames = diagrams.map((d) => d.name);
let duplicatedName = `${diagram.name} - Copy`;
let counter = 1;
while (existingNames.includes(duplicatedName)) {
duplicatedName = `${diagram.name} - Copy ${counter}`;
counter++;
}
const diagramToAdd = {
...clonedDiagram,
name: duplicatedName,
createdAt: new Date(),
updatedAt: new Date(),
};
// Add 2 second delay for better UX
await new Promise((resolve) => setTimeout(resolve, 2000));
await addDiagram({ diagram: diagramToAdd });
// Clear current selection first, then select the new diagram
if (onSelectDiagram) {
onSelectDiagram(undefined); // Clear selection
await refetch(); // Refresh the list
// Use setTimeout to ensure the DOM has updated with the new row
setTimeout(() => {
onSelectDiagram(diagramToAdd.id);
}, 100);
} else {
await refetch(); // Refresh the list
}
} catch (error) {
console.error('Error duplicating diagram:', error);
} finally {
setIsDuplicating(false);
}
}, [
diagram,
addDiagram,
listDiagrams,
getDiagram,
refetch,
onSelectDiagram,
]);
const handleDeleteDiagram = useCallback(() => {
showAlert({
title: t('delete_diagram_alert.title'),
description: t('delete_diagram_alert.description'),
actionLabel: t('delete_diagram_alert.delete'),
closeLabel: t('delete_diagram_alert.cancel'),
onAction: async () => {
await deleteDiagram(diagram.id);
// If we deleted the currently open diagram, navigate to another one
if (currentDiagramId === diagram.id) {
// Get updated list of diagrams after deletion
const remainingDiagrams = await listDiagrams();
if (remainingDiagrams.length > 0) {
// Sort by last modified date (most recent first)
const sortedDiagrams = remainingDiagrams.sort(
(a, b) =>
b.updatedAt.getTime() - a.updatedAt.getTime()
);
// Navigate to the most recently modified diagram
const firstDiagram = sortedDiagrams[0];
updateConfig({
config: { defaultDiagramId: firstDiagram.id },
});
navigate(`/diagrams/${firstDiagram.id}`);
} else {
// No diagrams left, navigate to home
navigate('/');
}
}
refetch(); // Refresh the list
},
});
}, [
diagram.id,
currentDiagramId,
deleteDiagram,
refetch,
showAlert,
t,
listDiagrams,
updateConfig,
navigate,
]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="size-8 p-0"
onClick={(e) => e.stopPropagation()}
disabled={isDuplicating}
>
{isDuplicating ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
<MoreHorizontal className="size-3.5" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onOpen();
}}
className="flex justify-between gap-4"
>
Open
<SquareArrowOutUpRight className="size-3.5" />
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleDuplicateDiagram();
}}
className="flex justify-between gap-4"
>
{t('menu.databases.duplicate')}
<Copy className="size-3.5" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleDeleteDiagram();
}}
className="flex items-center justify-between text-red-600 focus:text-red-600"
>
{t('menu.databases.delete_diagram')}
<Trash2 className="size-3.5" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -27,6 +27,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useDebounce } from '@/hooks/use-debounce';
import { DiagramRowActionsMenu } from './diagram-row-actions-menu/diagram-row-actions-menu';
export interface OpenDiagramDialogProps extends BaseDialogProps {
canClose?: boolean;
@@ -50,17 +51,18 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
setSelectedDiagramId(undefined);
}, [dialog.open]);
const fetchDiagrams = useCallback(async () => {
const diagrams = await listDiagrams({ includeTables: true });
setDiagrams(
diagrams.sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
)
);
}, [listDiagrams]);
useEffect(() => {
const fetchDiagrams = async () => {
const diagrams = await listDiagrams({ includeTables: true });
setDiagrams(
diagrams.sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
)
);
};
fetchDiagrams();
}, [listDiagrams, setDiagrams, dialog.open]);
}, [fetchDiagrams, dialog.open]);
const openDiagram = useCallback(
(diagramId: string) => {
@@ -221,6 +223,19 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
<TableCell className="text-center">
{diagram.tables?.length}
</TableCell>
<TableCell className="items-center p-0 pr-1 text-right">
<DiagramRowActionsMenu
diagram={diagram}
onOpen={() => {
openDiagram(diagram.id);
closeOpenDiagramDialog();
}}
refetch={fetchDiagrams}
onSelectDiagram={
setSelectedDiagramId
}
/>
</TableCell>
</TableRow>
))}
</TableBody>

View File

@@ -17,6 +17,7 @@ export const ar: LanguageTranslation = {
new: 'مخطط جديد',
browse: 'تصفح...',
save: 'حفظ',
duplicate: 'تكرار',
import: 'استيراد قاعدة بيانات',
export_sql: 'SQL تصدير',
export_as: 'تصدير كـ',

View File

@@ -17,6 +17,7 @@ export const bn: LanguageTranslation = {
new: 'নতুন ডায়াগ্রাম',
browse: 'ব্রাউজ করুন...',
save: 'সংরক্ষণ করুন',
duplicate: 'ডুপ্লিকেট করুন',
import: 'ডাটাবেস আমদানি করুন',
export_sql: 'SQL রপ্তানি করুন',
export_as: 'রূপে রপ্তানি করুন',

View File

@@ -17,6 +17,7 @@ export const de: LanguageTranslation = {
new: 'Neues Diagramm',
browse: 'Durchsuchen...',
save: 'Speichern',
duplicate: 'Diagramm duplizieren',
import: 'Datenbank importieren',
export_sql: 'SQL exportieren',
export_as: 'Exportieren als',
@@ -304,7 +305,7 @@ export const de: LanguageTranslation = {
step_1: 'Gehen Sie zu Tools > Optionen > Abfrageergebnisse > SQL Server.',
step_2: 'Wenn Sie "Ergebnisse in Raster" verwenden, ändern Sie die maximale Zeichenanzahl für Nicht-XML-Daten (auf 9999999 setzen).',
},
instructions_link: 'Brauchen Sie Hilfe? So gehts',
instructions_link: "Brauchen Sie Hilfe? So geht's",
check_script_result: 'Skriptergebnis überprüfen',
},

View File

@@ -17,6 +17,7 @@ export const en = {
new: 'New Diagram',
browse: 'Browse...',
save: 'Save',
duplicate: 'Duplicate Diagram',
import: 'Import',
export_sql: 'Export SQL',
export_as: 'Export as',

View File

@@ -17,6 +17,7 @@ export const es: LanguageTranslation = {
new: 'Nuevo Diagrama',
browse: 'Examinar...',
save: 'Guardar',
duplicate: 'Duplicar',
import: 'Importar Base de Datos',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',

View File

@@ -17,6 +17,7 @@ export const fr: LanguageTranslation = {
new: 'Nouveau Diagramme',
browse: 'Parcourir...',
save: 'Enregistrer',
duplicate: 'Dupliquer',
import: 'Importer Base de Données',
export_sql: 'Exporter SQL',
export_as: 'Exporter en tant que',

View File

@@ -17,6 +17,7 @@ export const gu: LanguageTranslation = {
new: 'નવું ડાયાગ્રામ',
browse: 'બ્રાઉજ કરો...',
save: 'સાચવો',
duplicate: 'ડુપ્લિકેટ',
import: 'ડેટાબેસ આયાત કરો',
export_sql: 'SQL નિકાસ કરો',
export_as: 'રૂપે નિકાસ કરો',

View File

@@ -17,6 +17,7 @@ export const hi: LanguageTranslation = {
new: 'नया आरेख',
browse: 'ब्राउज़ करें...',
save: 'सहेजें',
duplicate: 'डुप्लिकेट',
import: 'डेटाबेस आयात करें',
export_sql: 'SQL निर्यात करें',
export_as: 'के रूप में निर्यात करें',

View File

@@ -17,6 +17,7 @@ export const hr: LanguageTranslation = {
new: 'Novi Dijagram',
browse: 'Pregledaj...',
save: 'Spremi',
duplicate: 'Dupliciraj dijagram',
import: 'Uvezi',
export_sql: 'Izvezi SQL',
export_as: 'Izvezi kao',

View File

@@ -17,6 +17,7 @@ export const id_ID: LanguageTranslation = {
new: 'Diagram Baru',
browse: 'Jelajahi...',
save: 'Simpan',
duplicate: 'Duplikat',
import: 'Impor Database',
export_sql: 'Ekspor SQL',
export_as: 'Ekspor Sebagai',

View File

@@ -17,6 +17,7 @@ export const ja: LanguageTranslation = {
new: '新しいダイアグラム',
browse: '参照...',
save: '保存',
duplicate: '複製',
import: 'データベースをインポート',
export_sql: 'SQLをエクスポート',
export_as: '形式を指定してエクスポート',

View File

@@ -17,6 +17,7 @@ export const ko_KR: LanguageTranslation = {
new: '새 다이어그램',
browse: '찾아보기...',
save: '저장',
duplicate: '복사',
import: '데이터베이스 가져오기',
export_sql: 'SQL로 저장',
export_as: '다른 형식으로 저장',

View File

@@ -17,6 +17,7 @@ export const mr: LanguageTranslation = {
new: 'नवीन आरेख',
browse: 'ब्राउज करा...',
save: 'जतन करा',
duplicate: 'डुप्लिकेट',
import: 'डेटाबेस इम्पोर्ट करा',
export_sql: 'SQL एक्स्पोर्ट करा',
export_as: 'म्हणून एक्स्पोर्ट करा',

View File

@@ -17,6 +17,7 @@ export const ne: LanguageTranslation = {
new: 'नयाँ डायाग्राम',
browse: 'ब्राउज गर्नुहोस्...',
save: 'सुरक्षित गर्नुहोस्',
duplicate: 'डुप्लिकेट',
import: 'डाटाबेस आयात गर्नुहोस्',
export_sql: 'SQL निर्यात गर्नुहोस्',
export_as: 'निर्यात गर्नुहोस्',

View File

@@ -17,6 +17,7 @@ export const pt_BR: LanguageTranslation = {
new: 'Novo Diagrama',
browse: 'Navegar...',
save: 'Salvar',
duplicate: 'Duplicar',
import: 'Importar Banco de Dados',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',

View File

@@ -17,6 +17,7 @@ export const ru: LanguageTranslation = {
new: 'Новая диаграмма',
browse: 'Обзор...',
save: 'Сохранить',
duplicate: 'Дублировать',
import: 'Импортировать базу данных',
export_sql: 'Экспорт SQL',
export_as: 'Экспортировать как',

View File

@@ -17,6 +17,7 @@ export const te: LanguageTranslation = {
new: 'కొత్త డైగ్రాం',
browse: 'బ్రాఉజ్ చేయండి...',
save: 'సేవ్',
duplicate: 'డుప్లికేట్',
import: 'డేటాబేస్‌ను దిగుమతి చేసుకోండి',
export_sql: 'SQL ఎగుమతి',
export_as: 'వగా ఎగుమతి చేయండి',

View File

@@ -17,6 +17,7 @@ export const tr: LanguageTranslation = {
new: 'Yeni Diyagram',
browse: 'Gözat...',
save: 'Kaydet',
duplicate: 'Kopyala',
import: 'Veritabanı İçe Aktar',
export_sql: 'SQL Olarak Dışa Aktar',
export_as: 'Olarak Dışa Aktar',

View File

@@ -17,6 +17,7 @@ export const uk: LanguageTranslation = {
new: 'Нова діаграма',
browse: 'Огляд...',
save: 'Зберегти',
duplicate: 'Дублювати',
import: 'Імпорт бази даних',
export_sql: 'Експорт SQL',
export_as: 'Експортувати як',

View File

@@ -17,6 +17,7 @@ export const vi: LanguageTranslation = {
new: 'Sơ đồ mới',
browse: 'Duyệt...',
save: 'Lưu',
duplicate: 'Nhân đôi',
import: 'Nhập cơ sở dữ liệu',
export_sql: 'Xuất SQL',
export_as: 'Xuất thành',

View File

@@ -17,6 +17,7 @@ export const zh_CN: LanguageTranslation = {
new: '新建关系图',
browse: '浏览...',
save: '保存',
duplicate: '复制',
import: '导入数据库',
export_sql: '导出 SQL 语句',
export_as: '导出为',

View File

@@ -17,6 +17,7 @@ export const zh_TW: LanguageTranslation = {
new: '新增圖表',
browse: '瀏覽...',
save: '儲存',
duplicate: '複製',
import: '匯入資料庫',
export_sql: '匯出 SQL',
export_as: '匯出為特定格式',