mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-23 22:48:57 -05:00
feat: read-only mode (#168)
* add react router * add share button * working version * loaded diagram metadata and avoid redunant code * add multilingual support * remove styling * remove diagram storage * change failed load page to alert * remove loading condition * remove unused translations * prevent interactive mode from flashing up during readonly redirect * improve check for readonly
This commit is contained in:
88
package-lock.json
generated
88
package-lock.json
generated
@@ -3703,9 +3703,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
|
||||
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
|
||||
"version": "1.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
|
||||
"integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -17365,35 +17365,50 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.30.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
|
||||
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
|
||||
"integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.0"
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.30.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
|
||||
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz",
|
||||
"integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.0",
|
||||
"react-router": "6.30.1"
|
||||
"react-router": "7.9.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router/node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-smooth": {
|
||||
@@ -18263,6 +18278,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
@@ -20917,6 +20938,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-i18next": "^15.7.4",
|
||||
"react-router-dom": "^7.9.6",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -21563,6 +21585,38 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"packages/fossflow-lib/node_modules/react-router-dom": {
|
||||
"version": "6.30.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz",
|
||||
"integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.1",
|
||||
"react-router": "6.30.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"packages/fossflow-lib/node_modules/react-router-dom/node_modules/react-router": {
|
||||
"version": "6.30.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz",
|
||||
"integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"packages/fossflow-lib/node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-i18next": "^15.7.4",
|
||||
"react-router-dom": "^7.9.6",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "রপ্তানি করা JSON ফাইলগুলি পরে আমদানি করা যেতে পারে বা অন্যদের সাথে শেয়ার করা যেতে পারে।",
|
||||
"btnDownload": "JSON ডাউনলোড করুন",
|
||||
"btnCancel": "বাতিল করুন"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "শুধুমাত্র দেখার মোড",
|
||||
"failed": "ডায়াগ্রাম লোড করতে ব্যর্থ"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "Exported JSON files can be imported later or shared with others.",
|
||||
"btnDownload": "Download JSON",
|
||||
"btnCancel": "Cancel"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "View-Only Mode",
|
||||
"failed": "Failed to load diagram"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "Los archivos JSON exportados pueden importarse posteriormente o compartirse con otros.",
|
||||
"btnDownload": "Descargar JSON",
|
||||
"btnCancel": "Cancelar"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "Modo de solo lectura",
|
||||
"failed": "Error al cargar el diagrama"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "Les fichiers JSON exportés peuvent être importés ultérieurement ou partagés avec d'autres.",
|
||||
"btnDownload": "Télécharger JSON",
|
||||
"btnCancel": "Annuler"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "Mode lecture seule",
|
||||
"failed": "Échec du chargement du diagramme"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "निर्यात की गई JSON फ़ाइलों को बाद में आयात किया जा सकता है या दूसरों के साथ साझा किया जा सकता है।",
|
||||
"btnDownload": "JSON डाउनलोड करें",
|
||||
"btnCancel": "रद्द करें"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "केवल देखने का मोड",
|
||||
"failed": "आरेख लोड करने में विफल"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "I file JSON esportati possono essere importati in seguito o condivisi con altri.",
|
||||
"btnDownload": "Scarica JSON",
|
||||
"btnCancel": "Annulla"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "Modalità sola lettura",
|
||||
"failed": "Impossibile caricare il diagramma"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "Arquivos JSON exportados podem ser importados posteriormente ou compartilhados com outros.",
|
||||
"btnDownload": "Baixar JSON",
|
||||
"btnCancel": "Cancelar"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "Modo somente leitura",
|
||||
"failed": "Falha ao carregar diagrama"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "Экспортированные файлы JSON можно импортировать позже или поделиться с другими.",
|
||||
"btnDownload": "Скачать JSON",
|
||||
"btnCancel": "Отмена"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "Режим только для чтения",
|
||||
"failed": "Не удалось загрузить диаграмму"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "导出的 JSON 文件可以稍后导入或与他人共享。",
|
||||
"btnDownload": "下载 JSON",
|
||||
"btnCancel": "取消"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "阅读模式",
|
||||
"failed": "加载图表失败"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -3,7 +3,11 @@ import { Isoflow } from 'fossflow';
|
||||
import { flattenCollections } from '@isoflow/isopacks/dist/utils';
|
||||
import isoflowIsopack from '@isoflow/isopacks/dist/isoflow';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DiagramData, mergeDiagramData, extractSavableData } from './diagramUtils';
|
||||
import {
|
||||
DiagramData,
|
||||
mergeDiagramData,
|
||||
extractSavableData
|
||||
} from './diagramUtils';
|
||||
import { StorageManager } from './StorageManager';
|
||||
import { DiagramManager } from './components/DiagramManager';
|
||||
import { storageManager } from './services/storageService';
|
||||
@@ -11,11 +15,11 @@ import ChangeLanguage from './components/ChangeLanguage';
|
||||
import { allLocales } from 'fossflow';
|
||||
import { useIconPackManager, IconPackName } from './services/iconPackManager';
|
||||
import './App.css';
|
||||
import { BrowserRouter, Route, Routes, useParams } from 'react-router-dom';
|
||||
|
||||
// Load core isoflow icons (always loaded)
|
||||
const coreIcons = flattenCollections([isoflowIsopack]);
|
||||
|
||||
|
||||
interface SavedDiagram {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -25,11 +29,25 @@ interface SavedDiagram {
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<EditorPage />} />
|
||||
<Route path="/display/:readonlyDiagramId" element={<EditorPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
function EditorPage() {
|
||||
// Initialize icon pack manager with core icons
|
||||
const iconPackManager = useIconPackManager(coreIcons);
|
||||
const { readonlyDiagramId } = useParams<{ readonlyDiagramId: string }>();
|
||||
|
||||
const [diagrams, setDiagrams] = useState<SavedDiagram[]>([]);
|
||||
const [currentDiagram, setCurrentDiagram] = useState<SavedDiagram | null>(null);
|
||||
const [currentDiagram, setCurrentDiagram] = useState<SavedDiagram | null>(
|
||||
null
|
||||
);
|
||||
const [diagramName, setDiagramName] = useState('');
|
||||
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
||||
const [showLoadDialog, setShowLoadDialog] = useState(false);
|
||||
@@ -41,7 +59,9 @@ function App() {
|
||||
const [showStorageManager, setShowStorageManager] = useState(false);
|
||||
const [showDiagramManager, setShowDiagramManager] = useState(false);
|
||||
const [serverStorageAvailable, setServerStorageAvailable] = useState(false);
|
||||
|
||||
const isReadonlyUrl =
|
||||
window.location.pathname.startsWith('/display/') && readonlyDiagramId;
|
||||
|
||||
// Initialize with empty diagram data
|
||||
// Create default colors for connectors
|
||||
const defaultColors = [
|
||||
@@ -53,15 +73,16 @@ function App() {
|
||||
{ id: 'black', value: '#000000' },
|
||||
{ id: 'gray', value: '#666666' }
|
||||
];
|
||||
|
||||
|
||||
|
||||
const [diagramData, setDiagramData] = useState<DiagramData>(() => {
|
||||
// Initialize with last opened data if available
|
||||
const lastOpenedData = localStorage.getItem('fossflow-last-opened-data');
|
||||
if (lastOpenedData) {
|
||||
try {
|
||||
const data = JSON.parse(lastOpenedData);
|
||||
const importedIcons = (data.icons || []).filter((icon: any) => icon.collection === 'imported');
|
||||
const importedIcons = (data.icons || []).filter((icon: any) => {
|
||||
return icon.collection === 'imported';
|
||||
});
|
||||
const mergedIcons = [...coreIcons, ...importedIcons];
|
||||
return {
|
||||
...data,
|
||||
@@ -87,20 +108,59 @@ function App() {
|
||||
|
||||
// Check for server storage availability
|
||||
useEffect(() => {
|
||||
storageManager.initialize().then(() => {
|
||||
setServerStorageAvailable(storageManager.isServerStorage());
|
||||
}).catch(console.error);
|
||||
storageManager
|
||||
.initialize()
|
||||
.then(() => {
|
||||
setServerStorageAvailable(storageManager.isServerStorage());
|
||||
})
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
// Check if readonlyDiagramId exists - if exists, load diagram in view-only mode
|
||||
useEffect(() => {
|
||||
if (!isReadonlyUrl || !serverStorageAvailable) return;
|
||||
const loadReadonlyDiagram = async () => {
|
||||
try {
|
||||
const storage = storageManager.getStorage();
|
||||
// Get diagram metadata
|
||||
const diagramList = await storage.listDiagrams();
|
||||
const diagramInfo = diagramList.find((d) => {
|
||||
return d.id === readonlyDiagramId;
|
||||
});
|
||||
// Load the diagram data from server storage
|
||||
const data = await storage.loadDiagram(readonlyDiagramId);
|
||||
// Convert to SavedDiagram interface format
|
||||
const readonlyDiagram: SavedDiagram = {
|
||||
id: readonlyDiagramId,
|
||||
name: diagramInfo?.name || data.title || 'Readonly Diagram',
|
||||
data: data,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt:
|
||||
diagramInfo?.lastModified.toISOString() || new Date().toISOString()
|
||||
};
|
||||
await loadDiagram(readonlyDiagram, true);
|
||||
} catch (error) {
|
||||
// Alert if unable to load readonly diagram and redirect to new diagram
|
||||
alert(t('dialog.readOnly.failed'));
|
||||
window.location.href = '/';
|
||||
}
|
||||
};
|
||||
loadReadonlyDiagram();
|
||||
}, [readonlyDiagramId, serverStorageAvailable]);
|
||||
|
||||
// Update diagramData when loaded icons change
|
||||
useEffect(() => {
|
||||
setDiagramData(prev => ({
|
||||
...prev,
|
||||
icons: [
|
||||
...iconPackManager.loadedIcons,
|
||||
...(prev.icons || []).filter(icon => icon.collection === 'imported')
|
||||
]
|
||||
}));
|
||||
setDiagramData((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
icons: [
|
||||
...iconPackManager.loadedIcons,
|
||||
...(prev.icons || []).filter((icon) => {
|
||||
return icon.collection === 'imported';
|
||||
})
|
||||
]
|
||||
};
|
||||
});
|
||||
}, [iconPackManager.loadedIcons]);
|
||||
|
||||
// Load diagrams from localStorage on component mount
|
||||
@@ -109,14 +169,16 @@ function App() {
|
||||
if (savedDiagrams) {
|
||||
setDiagrams(JSON.parse(savedDiagrams));
|
||||
}
|
||||
|
||||
|
||||
// Load last opened diagram metadata (data is already loaded in state initialization)
|
||||
const lastOpenedId = localStorage.getItem('fossflow-last-opened');
|
||||
|
||||
|
||||
if (lastOpenedId && savedDiagrams) {
|
||||
try {
|
||||
const allDiagrams = JSON.parse(savedDiagrams);
|
||||
const lastDiagram = allDiagrams.find((d: SavedDiagram) => d.id === lastOpenedId);
|
||||
const lastDiagram = allDiagrams.find((d: SavedDiagram) => {
|
||||
return d.id === lastOpenedId;
|
||||
});
|
||||
if (lastDiagram) {
|
||||
setCurrentDiagram(lastDiagram);
|
||||
setDiagramName(lastDiagram.name);
|
||||
@@ -129,18 +191,23 @@ function App() {
|
||||
}
|
||||
}, [diagramData]);
|
||||
|
||||
// Save diagrams to localStorage whenever they change
|
||||
// Save diagrams to localStorage whenever they change
|
||||
useEffect(() => {
|
||||
try {
|
||||
// Store diagrams without the full icon data
|
||||
const diagramsToStore = diagrams.map(d => ({
|
||||
...d,
|
||||
data: {
|
||||
...d.data,
|
||||
icons: [] // Don't store icons with each diagram
|
||||
}
|
||||
}));
|
||||
localStorage.setItem('fossflow-diagrams', JSON.stringify(diagramsToStore));
|
||||
const diagramsToStore = diagrams.map((d) => {
|
||||
return {
|
||||
...d,
|
||||
data: {
|
||||
...d.data,
|
||||
icons: [] // Don't store icons with each diagram
|
||||
}
|
||||
};
|
||||
});
|
||||
localStorage.setItem(
|
||||
'fossflow-diagrams',
|
||||
JSON.stringify(diagramsToStore)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to save diagrams:', e);
|
||||
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
|
||||
@@ -156,10 +223,10 @@ function App() {
|
||||
}
|
||||
|
||||
// Check if a diagram with this name already exists (excluding current)
|
||||
const existingDiagram = diagrams.find(d =>
|
||||
d.name === diagramName.trim() && d.id !== currentDiagram?.id
|
||||
);
|
||||
|
||||
const existingDiagram = diagrams.find((d) => {
|
||||
return d.name === diagramName.trim() && d.id !== currentDiagram?.id;
|
||||
});
|
||||
|
||||
if (existingDiagram) {
|
||||
const confirmOverwrite = window.confirm(
|
||||
t('alert.diagramExists', { name: diagramName })
|
||||
@@ -170,9 +237,14 @@ function App() {
|
||||
}
|
||||
|
||||
// Construct save data - include only imported icons
|
||||
const importedIcons = (currentModel?.icons || diagramData.icons || [])
|
||||
.filter(icon => icon.collection === 'imported');
|
||||
|
||||
const importedIcons = (
|
||||
currentModel?.icons ||
|
||||
diagramData.icons ||
|
||||
[]
|
||||
).filter((icon) => {
|
||||
return icon.collection === 'imported';
|
||||
});
|
||||
|
||||
const savedData = {
|
||||
title: diagramName,
|
||||
icons: importedIcons, // Save only imported icons with diagram
|
||||
@@ -181,7 +253,6 @@ function App() {
|
||||
views: currentModel?.views || diagramData.views || [],
|
||||
fitToScreen: true
|
||||
};
|
||||
|
||||
|
||||
const newDiagram: SavedDiagram = {
|
||||
id: currentDiagram?.id || Date.now().toString(),
|
||||
@@ -193,10 +264,24 @@ function App() {
|
||||
|
||||
if (currentDiagram) {
|
||||
// Update existing diagram
|
||||
setDiagrams(diagrams.map(d => d.id === currentDiagram.id ? newDiagram : d));
|
||||
setDiagrams(
|
||||
diagrams.map((d) => {
|
||||
return d.id === currentDiagram.id ? newDiagram : d;
|
||||
})
|
||||
);
|
||||
} else if (existingDiagram) {
|
||||
// Replace existing diagram with same name
|
||||
setDiagrams(diagrams.map(d => d.id === existingDiagram.id ? { ...newDiagram, id: existingDiagram.id, createdAt: existingDiagram.createdAt } : d));
|
||||
setDiagrams(
|
||||
diagrams.map((d) => {
|
||||
return d.id === existingDiagram.id
|
||||
? {
|
||||
...newDiagram,
|
||||
id: existingDiagram.id,
|
||||
createdAt: existingDiagram.createdAt
|
||||
}
|
||||
: d;
|
||||
})
|
||||
);
|
||||
newDiagram.id = existingDiagram.id;
|
||||
newDiagram.createdAt = existingDiagram.createdAt;
|
||||
} else {
|
||||
@@ -208,11 +293,14 @@ function App() {
|
||||
setShowSaveDialog(false);
|
||||
setHasUnsavedChanges(false);
|
||||
setLastAutoSave(new Date());
|
||||
|
||||
|
||||
// Save as last opened
|
||||
try {
|
||||
localStorage.setItem('fossflow-last-opened', newDiagram.id);
|
||||
localStorage.setItem('fossflow-last-opened-data', JSON.stringify(newDiagram.data));
|
||||
localStorage.setItem(
|
||||
'fossflow-last-opened-data',
|
||||
JSON.stringify(newDiagram.data)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to save diagram:', e);
|
||||
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
|
||||
@@ -222,8 +310,15 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadDiagram = async (diagram: SavedDiagram) => {
|
||||
if (hasUnsavedChanges && !window.confirm(t('alert.unsavedChanges'))) {
|
||||
const loadDiagram = async (
|
||||
diagram: SavedDiagram,
|
||||
skipUnsavedCheck = false
|
||||
) => {
|
||||
if (
|
||||
!skipUnsavedCheck &&
|
||||
hasUnsavedChanges &&
|
||||
!window.confirm(t('alert.unsavedChanges'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -231,25 +326,32 @@ function App() {
|
||||
await iconPackManager.loadPacksForDiagram(diagram.data.items || []);
|
||||
|
||||
// Merge imported icons with loaded icon set
|
||||
const importedIcons = (diagram.data.icons || []).filter((icon: any) => icon.collection === 'imported');
|
||||
const importedIcons = (diagram.data.icons || []).filter((icon: any) => {
|
||||
return icon.collection === 'imported';
|
||||
});
|
||||
const mergedIcons = [...iconPackManager.loadedIcons, ...importedIcons];
|
||||
const dataWithIcons = {
|
||||
...diagram.data,
|
||||
icons: mergedIcons
|
||||
};
|
||||
|
||||
|
||||
setCurrentDiagram(diagram);
|
||||
setDiagramName(diagram.name);
|
||||
setDiagramData(dataWithIcons);
|
||||
setCurrentModel(dataWithIcons);
|
||||
setFossflowKey(prev => prev + 1); // Force re-render of FossFLOW
|
||||
setFossflowKey((prev) => {
|
||||
return prev + 1;
|
||||
}); // Force re-render of FossFLOW
|
||||
setShowLoadDialog(false);
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
|
||||
// Save as last opened (without icons)
|
||||
try {
|
||||
localStorage.setItem('fossflow-last-opened', diagram.id);
|
||||
localStorage.setItem('fossflow-last-opened-data', JSON.stringify(diagram.data));
|
||||
localStorage.setItem(
|
||||
'fossflow-last-opened-data',
|
||||
JSON.stringify(diagram.data)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to save last opened:', e);
|
||||
}
|
||||
@@ -257,7 +359,11 @@ function App() {
|
||||
|
||||
const deleteDiagram = (id: string) => {
|
||||
if (window.confirm(t('alert.confirmDelete'))) {
|
||||
setDiagrams(diagrams.filter(d => d.id !== id));
|
||||
setDiagrams(
|
||||
diagrams.filter((d) => {
|
||||
return d.id !== id;
|
||||
})
|
||||
);
|
||||
if (currentDiagram?.id === id) {
|
||||
setCurrentDiagram(null);
|
||||
setDiagramName('');
|
||||
@@ -283,7 +389,9 @@ function App() {
|
||||
setDiagramName('');
|
||||
setDiagramData(emptyDiagram);
|
||||
setCurrentModel(emptyDiagram); // Reset current model too
|
||||
setFossflowKey(prev => prev + 1); // Force re-render of FossFLOW
|
||||
setFossflowKey((prev) => {
|
||||
return prev + 1;
|
||||
}); // Force re-render of FossFLOW
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
// Clear last opened
|
||||
@@ -295,7 +403,7 @@ function App() {
|
||||
const handleModelUpdated = (model: any) => {
|
||||
// Store the current model state whenever it updates
|
||||
// The model from Isoflow contains the COMPLETE state including all icons
|
||||
|
||||
|
||||
// Simply store the complete model as-is since it has everything
|
||||
const updatedModel = {
|
||||
title: model.title || diagramName || 'Untitled',
|
||||
@@ -305,40 +413,45 @@ function App() {
|
||||
views: model.views || [],
|
||||
fitToScreen: true
|
||||
};
|
||||
|
||||
|
||||
setCurrentModel(updatedModel);
|
||||
setDiagramData(updatedModel);
|
||||
setHasUnsavedChanges(true);
|
||||
|
||||
if (!isReadonlyUrl) {
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
};
|
||||
|
||||
const exportDiagram = () => {
|
||||
// Use the most recent model data - prefer currentModel as it gets updated by handleModelUpdated
|
||||
const modelToExport = currentModel || diagramData;
|
||||
|
||||
|
||||
// Get ALL icons from the current model (which includes both default and imported)
|
||||
const allModelIcons = modelToExport.icons || [];
|
||||
|
||||
|
||||
// For safety, also check diagramData for any imported icons not in currentModel
|
||||
const diagramImportedIcons = (diagramData.icons || []).filter(icon => icon.collection === 'imported');
|
||||
|
||||
const diagramImportedIcons = (diagramData.icons || []).filter((icon) => {
|
||||
return icon.collection === 'imported';
|
||||
});
|
||||
|
||||
// Create a map to deduplicate icons by ID, preferring the ones from currentModel
|
||||
const iconMap = new Map();
|
||||
|
||||
|
||||
// First add all icons from the model (includes defaults + imported)
|
||||
allModelIcons.forEach(icon => {
|
||||
allModelIcons.forEach((icon) => {
|
||||
iconMap.set(icon.id, icon);
|
||||
});
|
||||
|
||||
|
||||
// Then add any imported icons from diagramData that might be missing
|
||||
diagramImportedIcons.forEach(icon => {
|
||||
diagramImportedIcons.forEach((icon) => {
|
||||
if (!iconMap.has(icon.id)) {
|
||||
iconMap.set(icon.id, icon);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Get all unique icons
|
||||
const allIcons = Array.from(iconMap.values());
|
||||
|
||||
|
||||
const exportData = {
|
||||
title: diagramName || modelToExport.title || 'Exported Diagram',
|
||||
icons: allIcons, // Include ALL icons (default + imported) for portability
|
||||
@@ -347,9 +460,9 @@ function App() {
|
||||
views: modelToExport.views || [],
|
||||
fitToScreen: true
|
||||
};
|
||||
|
||||
|
||||
const jsonString = JSON.stringify(exportData, null, 2);
|
||||
|
||||
|
||||
// Create a blob and download link
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -358,12 +471,11 @@ function App() {
|
||||
a.download = `${diagramName || 'diagram'}-${new Date().toISOString().split('T')[0]}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
|
||||
setShowExportDialog(false);
|
||||
setHasUnsavedChanges(false); // Mark as saved after export
|
||||
};
|
||||
|
||||
|
||||
const handleDiagramManagerLoad = async (id: string, data: any) => {
|
||||
console.log(`App: handleDiagramManagerLoad called for diagram ${id}`);
|
||||
|
||||
@@ -396,21 +508,31 @@ function App() {
|
||||
// For backward compatibility with old saves, we detect and merge
|
||||
|
||||
let finalIcons;
|
||||
const hasDefaultIcons = loadedIcons.some((icon: any) =>
|
||||
icon.collection === 'isoflow' || icon.collection === 'aws' || icon.collection === 'gcp'
|
||||
);
|
||||
const hasDefaultIcons = loadedIcons.some((icon: any) => {
|
||||
return (
|
||||
icon.collection === 'isoflow' ||
|
||||
icon.collection === 'aws' ||
|
||||
icon.collection === 'gcp'
|
||||
);
|
||||
});
|
||||
|
||||
if (hasDefaultIcons) {
|
||||
// New format: Server saved ALL icons (default + imported)
|
||||
// Use them directly to preserve any custom icon modifications
|
||||
console.log(`App: Using all ${loadedIcons.length} icons from server (includes defaults + imported)`);
|
||||
console.log(
|
||||
`App: Using all ${loadedIcons.length} icons from server (includes defaults + imported)`
|
||||
);
|
||||
finalIcons = loadedIcons;
|
||||
} else {
|
||||
// Old format: Server only saved imported icons
|
||||
// Merge imported icons with currently loaded icon packs
|
||||
const importedIcons = loadedIcons.filter((icon: any) => icon.collection === 'imported');
|
||||
const importedIcons = loadedIcons.filter((icon: any) => {
|
||||
return icon.collection === 'imported';
|
||||
});
|
||||
finalIcons = [...iconPackManager.loadedIcons, ...importedIcons];
|
||||
console.log(`App: Old format detected. Merged ${importedIcons.length} imported icons with ${iconPackManager.loadedIcons.length} defaults = ${finalIcons.length} total`);
|
||||
console.log(
|
||||
`App: Old format detected. Merged ${importedIcons.length} imported icons with ${iconPackManager.loadedIcons.length} defaults = ${finalIcons.length} total`
|
||||
);
|
||||
}
|
||||
|
||||
const mergedData: DiagramData = {
|
||||
@@ -441,26 +563,33 @@ function App() {
|
||||
// Update diagramData and key together
|
||||
// This ensures Isoflow gets the correct data with the new key
|
||||
setDiagramData(mergedData);
|
||||
setFossflowKey(prev => {
|
||||
setFossflowKey((prev) => {
|
||||
const newKey = prev + 1;
|
||||
console.log(`App: Updated fossflowKey from ${prev} to ${newKey}`);
|
||||
return newKey;
|
||||
});
|
||||
|
||||
console.log(`App: Finished loading diagram ${id}, final icon count: ${finalIcons.length}`);
|
||||
console.log(
|
||||
`App: Finished loading diagram ${id}, final icon count: ${finalIcons.length}`
|
||||
);
|
||||
};
|
||||
|
||||
// i18n
|
||||
const { t, i18n } = useTranslation('app');
|
||||
|
||||
|
||||
// Auto-save functionality
|
||||
useEffect(() => {
|
||||
if (!currentModel || !hasUnsavedChanges || !currentDiagram) return;
|
||||
|
||||
const autoSaveTimer = setTimeout(() => {
|
||||
// Include imported icons in auto-save
|
||||
const importedIcons = (currentModel?.icons || diagramData.icons || [])
|
||||
.filter(icon => icon.collection === 'imported');
|
||||
const importedIcons = (
|
||||
currentModel?.icons ||
|
||||
diagramData.icons ||
|
||||
[]
|
||||
).filter((icon) => {
|
||||
return icon.collection === 'imported';
|
||||
});
|
||||
|
||||
const savedData = {
|
||||
title: diagramName || currentDiagram.name,
|
||||
@@ -477,13 +606,18 @@ function App() {
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
setDiagrams(prevDiagrams =>
|
||||
prevDiagrams.map(d => d.id === currentDiagram.id ? updatedDiagram : d)
|
||||
);
|
||||
setDiagrams((prevDiagrams) => {
|
||||
return prevDiagrams.map((d) => {
|
||||
return d.id === currentDiagram.id ? updatedDiagram : d;
|
||||
});
|
||||
});
|
||||
|
||||
// Update last opened data
|
||||
try {
|
||||
localStorage.setItem('fossflow-last-opened-data', JSON.stringify(savedData));
|
||||
localStorage.setItem(
|
||||
'fossflow-last-opened-data',
|
||||
JSON.stringify(savedData)
|
||||
);
|
||||
setLastAutoSave(new Date());
|
||||
setHasUnsavedChanges(false);
|
||||
} catch (e) {
|
||||
@@ -495,9 +629,11 @@ function App() {
|
||||
}
|
||||
}, 5000); // Auto-save after 5 seconds of changes
|
||||
|
||||
return () => clearTimeout(autoSaveTimer);
|
||||
return () => {
|
||||
return clearTimeout(autoSaveTimer);
|
||||
};
|
||||
}, [currentModel, hasUnsavedChanges, currentDiagram, diagramName]);
|
||||
|
||||
|
||||
// Warn before closing if there are unsaved changes
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
@@ -509,7 +645,9 @@ function App() {
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => {
|
||||
return window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [hasUnsavedChanges]);
|
||||
|
||||
// Keyboard shortcuts
|
||||
@@ -536,52 +674,107 @@ function App() {
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
return window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [currentDiagram, hasUnsavedChanges]);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<div className="toolbar">
|
||||
<button onClick={newDiagram}>{t('nav.newDiagram')}</button>
|
||||
{serverStorageAvailable && (
|
||||
<button
|
||||
onClick={() => setShowDiagramManager(true)}
|
||||
style={{ backgroundColor: '#2196F3', color: 'white' }}
|
||||
>
|
||||
🌐 {t('nav.serverStorage')}
|
||||
</button>
|
||||
{!isReadonlyUrl && (
|
||||
<>
|
||||
<button onClick={newDiagram}>{t('nav.newDiagram')}</button>
|
||||
{serverStorageAvailable && (
|
||||
<button
|
||||
onClick={() => {
|
||||
return setShowDiagramManager(true);
|
||||
}}
|
||||
style={{ backgroundColor: '#2196F3', color: 'white' }}
|
||||
>
|
||||
🌐 {t('nav.serverStorage')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
return setShowSaveDialog(true);
|
||||
}}
|
||||
>
|
||||
{t('nav.saveSessionOnly')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
return setShowLoadDialog(true);
|
||||
}}
|
||||
>
|
||||
{t('nav.loadSessionOnly')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
return setShowExportDialog(true);
|
||||
}}
|
||||
style={{ backgroundColor: '#007bff' }}
|
||||
>
|
||||
💾 {t('nav.exportFile')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (currentDiagram && hasUnsavedChanges) {
|
||||
saveDiagram();
|
||||
}
|
||||
}}
|
||||
disabled={!currentDiagram || !hasUnsavedChanges}
|
||||
style={{
|
||||
backgroundColor:
|
||||
currentDiagram && hasUnsavedChanges ? '#ffc107' : '#6c757d',
|
||||
opacity: currentDiagram && hasUnsavedChanges ? 1 : 0.5,
|
||||
cursor:
|
||||
currentDiagram && hasUnsavedChanges
|
||||
? 'pointer'
|
||||
: 'not-allowed'
|
||||
}}
|
||||
title="Save to current session only"
|
||||
>
|
||||
{t('nav.quickSaveSession')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{isReadonlyUrl && (
|
||||
<div
|
||||
style={{
|
||||
color: 'black',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '4px',
|
||||
fontWeight: 'bold',
|
||||
border: '2px solid #000000'
|
||||
}}
|
||||
>
|
||||
{t('dialog.readOnly.mode')}
|
||||
</div>
|
||||
)}
|
||||
<button onClick={() => setShowSaveDialog(true)}>{t('nav.saveSessionOnly')}</button>
|
||||
<button onClick={() => setShowLoadDialog(true)}>{t('nav.loadSessionOnly')}</button>
|
||||
<button
|
||||
onClick={() => setShowExportDialog(true)}
|
||||
style={{ backgroundColor: '#007bff' }}
|
||||
>
|
||||
💾 {t('nav.exportFile')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (currentDiagram && hasUnsavedChanges) {
|
||||
saveDiagram();
|
||||
}
|
||||
}}
|
||||
disabled={!currentDiagram || !hasUnsavedChanges}
|
||||
style={{
|
||||
backgroundColor: currentDiagram && hasUnsavedChanges ? '#ffc107' : '#6c757d',
|
||||
opacity: currentDiagram && hasUnsavedChanges ? 1 : 0.5,
|
||||
cursor: currentDiagram && hasUnsavedChanges ? 'pointer' : 'not-allowed'
|
||||
}}
|
||||
title="Save to current session only"
|
||||
>
|
||||
{t('nav.quickSaveSession')}
|
||||
</button>
|
||||
<ChangeLanguage />
|
||||
<span className="current-diagram">
|
||||
{currentDiagram ? `${t('status.current')}: ${currentDiagram.name}` : diagramName || t('status.untitled')}
|
||||
{hasUnsavedChanges && <span style={{ color: '#ff9800', marginLeft: '10px' }}>• {t('status.modified')}</span>}
|
||||
<span style={{ fontSize: '12px', color: '#666', marginLeft: '10px' }}>
|
||||
({t('status.sessionStorageNote')})
|
||||
</span>
|
||||
{isReadonlyUrl ? (
|
||||
<span>
|
||||
{t('status.current')}: {diagramName}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{currentDiagram
|
||||
? `${t('status.current')}: ${currentDiagram.name}`
|
||||
: diagramName || t('status.untitled')}
|
||||
{hasUnsavedChanges && (
|
||||
<span style={{ color: '#ff9800', marginLeft: '10px' }}>
|
||||
• {t('status.modified')}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
style={{ fontSize: '12px', color: '#666', marginLeft: '10px' }}
|
||||
>
|
||||
({t('status.sessionStorageNote')})
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -590,7 +783,7 @@ function App() {
|
||||
key={fossflowKey}
|
||||
initialData={diagramData}
|
||||
onModelUpdated={handleModelUpdated}
|
||||
editorMode="EDITABLE"
|
||||
editorMode={isReadonlyUrl ? 'EXPLORABLE_READONLY' : 'EDITABLE'}
|
||||
locale={allLocales[i18n.language as keyof typeof allLocales]}
|
||||
iconPackManager={{
|
||||
lazyLoadingEnabled: iconPackManager.lazyLoadingEnabled,
|
||||
@@ -609,28 +802,45 @@ function App() {
|
||||
<div className="dialog-overlay">
|
||||
<div className="dialog">
|
||||
<h2>{t('dialog.save.title')}</h2>
|
||||
<div style={{
|
||||
backgroundColor: '#fff3cd',
|
||||
border: '1px solid #ffeeba',
|
||||
padding: '15px',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<strong>⚠️ {t('dialog.save.warningTitle')}:</strong> {t('dialog.save.warningMessage')}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#fff3cd',
|
||||
border: '1px solid #ffeeba',
|
||||
padding: '15px',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
>
|
||||
<strong>⚠️ {t('dialog.save.warningTitle')}:</strong>{' '}
|
||||
{t('dialog.save.warningMessage')}
|
||||
<br />
|
||||
<span dangerouslySetInnerHTML={{ __html: t('dialog.save.warningExport') }} />
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('dialog.save.warningExport')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('dialog.save.placeholder')}
|
||||
value={diagramName}
|
||||
onChange={(e) => setDiagramName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && saveDiagram()}
|
||||
onChange={(e) => {
|
||||
return setDiagramName(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
return e.key === 'Enter' && saveDiagram();
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="dialog-buttons">
|
||||
<button onClick={saveDiagram}>{t('dialog.save.btnSave')}</button>
|
||||
<button onClick={() => setShowSaveDialog(false)}>{t('dialog.save.btnCancel')}</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
return setShowSaveDialog(false);
|
||||
}}
|
||||
>
|
||||
{t('dialog.save.btnCancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -641,64 +851,100 @@ function App() {
|
||||
<div className="dialog-overlay">
|
||||
<div className="dialog">
|
||||
<h2>{t('dialog.load.title')}</h2>
|
||||
<div style={{
|
||||
backgroundColor: '#fff3cd',
|
||||
border: '1px solid #ffeeba',
|
||||
padding: '15px',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<strong>⚠️ {t('dialog.load.noteTitle')}:</strong> {t('dialog.load.noteMessage')}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#fff3cd',
|
||||
border: '1px solid #ffeeba',
|
||||
padding: '15px',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
>
|
||||
<strong>⚠️ {t('dialog.load.noteTitle')}:</strong>{' '}
|
||||
{t('dialog.load.noteMessage')}
|
||||
</div>
|
||||
<div className="diagram-list">
|
||||
{diagrams.length === 0 ? (
|
||||
<p>{t('dialog.load.noSavedDiagrams')}</p>
|
||||
) : (
|
||||
diagrams.map(diagram => (
|
||||
<div key={diagram.id} className="diagram-item">
|
||||
<div>
|
||||
<strong>{diagram.name}</strong>
|
||||
<br />
|
||||
<small>{t('dialog.load.updated')}: {new Date(diagram.updatedAt).toLocaleString()}</small>
|
||||
diagrams.map((diagram) => {
|
||||
return (
|
||||
<div key={diagram.id} className="diagram-item">
|
||||
<div>
|
||||
<strong>{diagram.name}</strong>
|
||||
<br />
|
||||
<small>
|
||||
{t('dialog.load.updated')}:{' '}
|
||||
{new Date(diagram.updatedAt).toLocaleString()}
|
||||
</small>
|
||||
</div>
|
||||
<div className="diagram-actions">
|
||||
<button
|
||||
onClick={() => {
|
||||
return loadDiagram(diagram, false);
|
||||
}}
|
||||
>
|
||||
{t('dialog.load.btnLoad')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
return deleteDiagram(diagram.id);
|
||||
}}
|
||||
>
|
||||
{t('dialog.load.btnDelete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="diagram-actions">
|
||||
<button onClick={() => loadDiagram(diagram)}>{t('dialog.load.btnLoad')}</button>
|
||||
<button onClick={() => deleteDiagram(diagram.id)}>{t('dialog.load.btnDelete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<div className="dialog-buttons">
|
||||
<button onClick={() => setShowLoadDialog(false)}>{t('dialog.load.btnClose')}</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
return setShowLoadDialog(false);
|
||||
}}
|
||||
>
|
||||
{t('dialog.load.btnClose')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Export Dialog */}
|
||||
{showExportDialog && (
|
||||
<div className="dialog-overlay">
|
||||
<div className="dialog">
|
||||
<h2>{t('dialog.export.title')}</h2>
|
||||
<div style={{
|
||||
backgroundColor: '#d4edda',
|
||||
border: '1px solid #c3e6cb',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#d4edda',
|
||||
border: '1px solid #c3e6cb',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
>
|
||||
<p style={{ margin: '0 0 10px 0' }}>
|
||||
<strong>✅ {t('dialog.export.recommendedTitle')}:</strong> {t('dialog.export.recommendedMessage')}
|
||||
<strong>✅ {t('dialog.export.recommendedTitle')}:</strong>{' '}
|
||||
{t('dialog.export.recommendedMessage')}
|
||||
</p>
|
||||
<p style={{ margin: 0, fontSize: '14px', color: '#155724' }}>
|
||||
{t('dialog.export.noteMessage')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="dialog-buttons">
|
||||
<button onClick={exportDiagram}>{t('dialog.export.btnDownload')}</button>
|
||||
<button onClick={() => setShowExportDialog(false)}>{t('dialog.export.btnCancel')}</button>
|
||||
<button onClick={exportDiagram}>
|
||||
{t('dialog.export.btnDownload')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
return setShowExportDialog(false);
|
||||
}}
|
||||
>
|
||||
{t('dialog.export.btnCancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -706,7 +952,11 @@ function App() {
|
||||
|
||||
{/* Storage Manager */}
|
||||
{showStorageManager && (
|
||||
<StorageManager onClose={() => setShowStorageManager(false)} />
|
||||
<StorageManager
|
||||
onClose={() => {
|
||||
return setShowStorageManager(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Diagram Manager */}
|
||||
@@ -715,7 +965,9 @@ function App() {
|
||||
onLoadDiagram={handleDiagramManagerLoad}
|
||||
currentDiagramId={currentDiagram?.id}
|
||||
currentDiagramData={currentModel || diagramData}
|
||||
onClose={() => setShowDiagramManager(false)}
|
||||
onClose={() => {
|
||||
return setShowDiagramManager(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -129,6 +129,15 @@
|
||||
background: #da190b;
|
||||
}
|
||||
|
||||
.action-button.share {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-button.share:hover {
|
||||
background: #0b7dda;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
|
||||
@@ -9,11 +9,11 @@ interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const DiagramManager: React.FC<Props> = ({
|
||||
onLoadDiagram,
|
||||
currentDiagramId,
|
||||
export const DiagramManager: React.FC<Props> = ({
|
||||
onLoadDiagram,
|
||||
currentDiagramId,
|
||||
currentDiagramData,
|
||||
onClose
|
||||
onClose
|
||||
}) => {
|
||||
const [diagrams, setDiagrams] = useState<DiagramInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -36,7 +36,9 @@ export const DiagramManager: React.FC<Props> = ({
|
||||
await storageManager.initialize();
|
||||
const isServer = storageManager.isServerStorage();
|
||||
setIsServerStorage(isServer);
|
||||
console.log(`DiagramManager: Using ${isServer ? 'server' : 'session'} storage`);
|
||||
console.log(
|
||||
`DiagramManager: Using ${isServer ? 'server' : 'session'} storage`
|
||||
);
|
||||
|
||||
// Load diagram list
|
||||
const storage = storageManager.getStorage();
|
||||
@@ -45,7 +47,8 @@ export const DiagramManager: React.FC<Props> = ({
|
||||
console.log(`DiagramManager: Loaded ${list.length} diagrams`);
|
||||
setDiagrams(list);
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Failed to load diagrams';
|
||||
const errorMsg =
|
||||
err instanceof Error ? err.message : 'Failed to load diagrams';
|
||||
console.error('DiagramManager error:', err);
|
||||
setError(errorMsg);
|
||||
} finally {
|
||||
@@ -66,7 +69,9 @@ export const DiagramManager: React.FC<Props> = ({
|
||||
onLoadDiagram(id, data);
|
||||
|
||||
// Small delay to ensure parent component finishes state updates
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await new Promise((resolve) => {
|
||||
return setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
onClose();
|
||||
} catch (err) {
|
||||
@@ -91,6 +96,24 @@ export const DiagramManager: React.FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyShareLink = (id: string) => {
|
||||
const shareUrl = `${window.location.origin}/display/${id}`;
|
||||
navigator.clipboard
|
||||
.writeText(shareUrl)
|
||||
.then(() => {
|
||||
alert(`Share link copied to clipboard:\n${shareUrl}`);
|
||||
})
|
||||
.catch(() => {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = shareUrl;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
alert(`Share link copied to clipboard:\n${shareUrl}`);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!saveName.trim()) {
|
||||
setError('Please enter a diagram name');
|
||||
@@ -101,9 +124,9 @@ export const DiagramManager: React.FC<Props> = ({
|
||||
const storage = storageManager.getStorage();
|
||||
|
||||
// Check if a diagram with this name already exists (excluding current diagram)
|
||||
const existingDiagram = diagrams.find(d =>
|
||||
d.name === saveName.trim() && d.id !== currentDiagramId
|
||||
);
|
||||
const existingDiagram = diagrams.find((d) => {
|
||||
return d.name === saveName.trim() && d.id !== currentDiagramId;
|
||||
});
|
||||
|
||||
if (existingDiagram) {
|
||||
const confirmOverwrite = window.confirm(
|
||||
@@ -132,8 +155,12 @@ export const DiagramManager: React.FC<Props> = ({
|
||||
name: saveName
|
||||
};
|
||||
|
||||
console.log(`DiagramManager: Saving diagram with ${dataToSave.icons?.length || 0} icons`);
|
||||
const importedCount = (dataToSave.icons || []).filter((icon: any) => icon.collection === 'imported').length;
|
||||
console.log(
|
||||
`DiagramManager: Saving diagram with ${dataToSave.icons?.length || 0} icons`
|
||||
);
|
||||
const importedCount = (dataToSave.icons || []).filter((icon: any) => {
|
||||
return icon.collection === 'imported';
|
||||
}).length;
|
||||
console.log(`DiagramManager: Including ${importedCount} imported icons`);
|
||||
|
||||
if (currentDiagramId) {
|
||||
@@ -157,11 +184,15 @@ export const DiagramManager: React.FC<Props> = ({
|
||||
<div className="diagram-manager">
|
||||
<div className="diagram-manager-header">
|
||||
<h2>Diagram Manager</h2>
|
||||
<button className="close-button" onClick={onClose}>×</button>
|
||||
<button className="close-button" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="storage-info">
|
||||
<span className={`storage-badge ${isServerStorage ? 'server' : 'local'}`}>
|
||||
<span
|
||||
className={`storage-badge ${isServerStorage ? 'server' : 'local'}`}
|
||||
>
|
||||
{isServerStorage ? '🌐 Server Storage' : '💾 Local Storage'}
|
||||
</span>
|
||||
{isServerStorage && (
|
||||
@@ -171,14 +202,10 @@ export const DiagramManager: React.FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<div className="diagram-manager-actions">
|
||||
<button
|
||||
<button
|
||||
className="action-button primary"
|
||||
onClick={() => {
|
||||
setSaveName(currentDiagramData?.name || 'Untitled Diagram');
|
||||
@@ -199,33 +226,49 @@ export const DiagramManager: React.FC<Props> = ({
|
||||
<p className="hint">Save your current diagram to get started</p>
|
||||
</div>
|
||||
) : (
|
||||
diagrams.map(diagram => (
|
||||
<div key={diagram.id} className="diagram-item">
|
||||
<div className="diagram-info">
|
||||
<h3>{diagram.name}</h3>
|
||||
<span className="diagram-meta">
|
||||
Last modified: {diagram.lastModified.toLocaleString()}
|
||||
{diagram.size && ` • ${(diagram.size / 1024).toFixed(1)} KB`}
|
||||
</span>
|
||||
diagrams.map((diagram) => {
|
||||
return (
|
||||
<div key={diagram.id} className="diagram-item">
|
||||
<div className="diagram-info">
|
||||
<h3>{diagram.name}</h3>
|
||||
<span className="diagram-meta">
|
||||
Last modified: {diagram.lastModified.toLocaleString()}
|
||||
{diagram.size &&
|
||||
` • ${(diagram.size / 1024).toFixed(1)} KB`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="diagram-actions">
|
||||
<button
|
||||
className="action-button"
|
||||
onClick={() => {
|
||||
return handleLoad(diagram.id);
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading...' : 'Load'}
|
||||
</button>
|
||||
<button
|
||||
className="action-button share"
|
||||
onClick={() => {
|
||||
return handleCopyShareLink(diagram.id);
|
||||
}}
|
||||
title="Copy shareable link"
|
||||
>
|
||||
Share
|
||||
</button>
|
||||
<button
|
||||
className="action-button danger"
|
||||
onClick={() => {
|
||||
return handleDelete(diagram.id);
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="diagram-actions">
|
||||
<button
|
||||
className="action-button"
|
||||
onClick={() => handleLoad(diagram.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading...' : 'Load'}
|
||||
</button>
|
||||
<button
|
||||
className="action-button danger"
|
||||
onClick={() => handleDelete(diagram.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -238,17 +281,27 @@ export const DiagramManager: React.FC<Props> = ({
|
||||
type="text"
|
||||
placeholder="Diagram name"
|
||||
value={saveName}
|
||||
onChange={(e) => setSaveName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
|
||||
onChange={(e) => {
|
||||
return setSaveName(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
return e.key === 'Enter' && handleSave();
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="dialog-buttons">
|
||||
<button onClick={handleSave}>Save</button>
|
||||
<button onClick={() => setShowSaveDialog(false)}>Cancel</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
return setShowSaveDialog(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,55 +11,55 @@ i18n
|
||||
fallbackLng: 'en-US',
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
escapeValue: false
|
||||
},
|
||||
ns: ['app'],
|
||||
backend: {
|
||||
loadPath: './i18n/{{ns}}/{{lng}}.json'
|
||||
loadPath: '/i18n/{{ns}}/{{lng}}.json'
|
||||
},
|
||||
detection: {
|
||||
order: ['localStorage'],
|
||||
caches: ['localStorage'],
|
||||
caches: ['localStorage']
|
||||
}
|
||||
});
|
||||
|
||||
export const supportedLanguages = [
|
||||
{
|
||||
label: 'English',
|
||||
value: 'en-US',
|
||||
value: 'en-US'
|
||||
},
|
||||
{
|
||||
label: '中文',
|
||||
value: 'zh-CN',
|
||||
value: 'zh-CN'
|
||||
},
|
||||
{
|
||||
label: 'Español',
|
||||
value: 'es-ES',
|
||||
value: 'es-ES'
|
||||
},
|
||||
{
|
||||
label: 'Português',
|
||||
value: 'pt-BR',
|
||||
value: 'pt-BR'
|
||||
},
|
||||
{
|
||||
label: 'Français',
|
||||
value: 'fr-FR',
|
||||
value: 'fr-FR'
|
||||
},
|
||||
{
|
||||
label: 'हिन्दी',
|
||||
value: 'hi-IN',
|
||||
value: 'hi-IN'
|
||||
},
|
||||
{
|
||||
label: 'বাংলা',
|
||||
value: 'bn-BD',
|
||||
value: 'bn-BD'
|
||||
},
|
||||
{
|
||||
label: 'Русский',
|
||||
value: 'ru-RU',
|
||||
value: 'ru-RU'
|
||||
},
|
||||
{
|
||||
label: 'Italian',
|
||||
value: 'it-IT',
|
||||
},
|
||||
value: 'it-IT'
|
||||
}
|
||||
];
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "রপ্তানি করা JSON ফাইলগুলি পরে আমদানি করা যেতে পারে বা অন্যদের সাথে শেয়ার করা যেতে পারে।",
|
||||
"btnDownload": "JSON ডাউনলোড করুন",
|
||||
"btnCancel": "বাতিল করুন"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "শুধুমাত্র দেখার মোড",
|
||||
"failed": "ডায়াগ্রাম লোড করতে ব্যর্থ"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "Exported JSON files can be imported later or shared with others.",
|
||||
"btnDownload": "Download JSON",
|
||||
"btnCancel": "Cancel"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "View-Only Mode",
|
||||
"failed": "Failed to load diagram"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "Los archivos JSON exportados pueden importarse posteriormente o compartirse con otros.",
|
||||
"btnDownload": "Descargar JSON",
|
||||
"btnCancel": "Cancelar"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "Modo de solo lectura",
|
||||
"failed": "Error al cargar el diagrama"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "Les fichiers JSON exportés peuvent être importés ultérieurement ou partagés avec d'autres.",
|
||||
"btnDownload": "Télécharger JSON",
|
||||
"btnCancel": "Annuler"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "Mode lecture seule",
|
||||
"failed": "Échec du chargement du diagramme"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "निर्यात की गई JSON फ़ाइलों को बाद में आयात किया जा सकता है या दूसरों के साथ साझा किया जा सकता है।",
|
||||
"btnDownload": "JSON डाउनलोड करें",
|
||||
"btnCancel": "रद्द करें"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "केवल देखने का मोड",
|
||||
"failed": "आरेख लोड करने में विफल"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "I file JSON esportati possono essere importati in seguito o condivisi con altri.",
|
||||
"btnDownload": "Scarica JSON",
|
||||
"btnCancel": "Annulla"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "Modalità sola lettura",
|
||||
"failed": "Impossibile caricare il diagramma"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "Arquivos JSON exportados podem ser importados posteriormente ou compartilhados com outros.",
|
||||
"btnDownload": "Baixar JSON",
|
||||
"btnCancel": "Cancelar"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "Modo somente leitura",
|
||||
"failed": "Falha ao carregar diagrama"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "Экспортированные файлы JSON можно импортировать позже или поделиться с другими.",
|
||||
"btnDownload": "Скачать JSON",
|
||||
"btnCancel": "Отмена"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "Режим только для чтения",
|
||||
"failed": "Не удалось загрузить диаграмму"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"noteMessage": "导出的 JSON 文件可以稍后导入或与他人共享。",
|
||||
"btnDownload": "下载 JSON",
|
||||
"btnCancel": "取消"
|
||||
},
|
||||
"readOnly": {
|
||||
"mode": "阅读模式",
|
||||
"failed": "加载图表失败"
|
||||
}
|
||||
},
|
||||
"alert": {
|
||||
|
||||
Reference in New Issue
Block a user