diff --git a/src/main/frontend/app/components/save-status-indicator.tsx b/src/main/frontend/app/components/save-status-indicator.tsx new file mode 100644 index 00000000..08a850dd --- /dev/null +++ b/src/main/frontend/app/components/save-status-indicator.tsx @@ -0,0 +1,26 @@ +import { useEffect, useState } from 'react' +import { useSaveStatusStore } from '~/stores/save-status-store' + +function getTimeAgo(date: Date): string { + const seconds = Math.floor((Date.now() - date.getTime()) / 1000) + if (seconds < 60) return 'just now' + if (seconds < 3600) return `${Math.floor(seconds / 60)}min ago` + return `${Math.floor(seconds / 3600)}h ago` +} + +export function SaveStatusIndicator() { + const saveStatus = useSaveStatusStore((state) => state.saveStatus) + const savedAt = useSaveStatusStore((state) => state.savedAt) + const [, setTick] = useState(0) + + useEffect(() => { + if (!savedAt || saveStatus !== 'saved') return + const id = setInterval(() => setTick((t) => t + 1), 30_000) + return () => clearInterval(id) + }, [savedAt, saveStatus]) + + if (saveStatus === 'saving') return ☁️ Saving... + if (saveStatus === 'saved' && savedAt) + return ☁️ Saved {getTimeAgo(savedAt)} + return null +} diff --git a/src/main/frontend/app/components/tabs/tab.tsx b/src/main/frontend/app/components/tabs/tab.tsx index 895b5118..5a87057d 100644 --- a/src/main/frontend/app/components/tabs/tab.tsx +++ b/src/main/frontend/app/components/tabs/tab.tsx @@ -19,18 +19,10 @@ export default function Tab({ name, configurationPath, icon, isSelected, onSelec onClose(event) } - const fileBasename = configurationPath - ? (configurationPath - .split(/[/\\]/) - .pop() - ?.replace(/\.[^/.]+$/, '') ?? name) - : name - const displayText = fileBasename === name ? name : `${fileBasename} / ${name}` - return (
  • - {displayText} + {name} { } export function TabsView({ tabs, activeTab, onSelectTab, onCloseTab }: TabsViewProps) { - const tabsElementReference = useRef(null) const tabsListReference = useRef(null) const shadowLeftReference = useRef(null) const shadowRightReference = useRef(null) const entries = Object.entries(tabs) as [T, TabData][] const calculateScrollShadows = useCallback(() => { - setTimeout(() => { - if ( - !tabsElementReference.current || - !tabsListReference.current || - !shadowLeftReference.current || - !shadowRightReference.current - ) { - return - } + if (!tabsListReference.current || !shadowLeftReference.current || !shadowRightReference.current) return - const scrollWidth = tabsListReference.current.scrollWidth - tabsElementReference.current.offsetWidth - const scrollLeft = tabsListReference.current.scrollLeft + const { scrollWidth, clientWidth, scrollLeft } = tabsListReference.current - if (scrollWidth <= 0) { - setShadows(0, 0) - return - } + if (scrollWidth <= clientWidth) { + setShadows(0, 0) + return + } - const currentScroll = scrollLeft / scrollWidth - setShadows(currentScroll, 1 - currentScroll) - }) + const maxScroll = scrollWidth - clientWidth + const currentScroll = scrollLeft / maxScroll + setShadows(currentScroll, 1 - currentScroll) }, []) useEffect(() => { calculateScrollShadows() - window.addEventListener('resize', calculateScrollShadows) - return () => window.removeEventListener('resize', calculateScrollShadows) - }, [calculateScrollShadows, tabs]) - useEffect(() => { - calculateScrollShadows() - window.addEventListener('resize', calculateScrollShadows) - return () => { - window.removeEventListener('resize', calculateScrollShadows) - } - }, [tabs, calculateScrollShadows]) + const resizeObserver = new ResizeObserver(calculateScrollShadows) + if (tabsListReference.current) resizeObserver.observe(tabsListReference.current) + + return () => resizeObserver.disconnect() + }, [calculateScrollShadows, tabs]) const setShadows = (left: number, right: number) => { if (shadowLeftReference.current) { @@ -64,7 +49,7 @@ export function TabsView({ tabs, activeTab, onSelectTab, onClo } return ( -
    +
    (useEditorTabStore.getState().activeTabFilePath) const [fileContent, setFileContent] = useState('') const [fileLanguage, setFileLanguage] = useState('xml') - const [saveStatus, setSaveStatus] = useState('idle') + const { setSaving, setSaved, setIdle } = useSaveStatusStore() const [leftTab, setLeftTab] = useState('files') const [editorMounted, setEditorMounted] = useState(false) const [xsdLoaded, setXsdLoaded] = useState(false) + const [buttonRightOffset, setButtonRightOffset] = useState(14) const editorReference = useRef[0] | null>(null) const monacoReference = useRef(null) const xsdContentRef = useRef(null) const errorDecorationsRef = useRef<{ clear: () => void } | null>(null) const flowDecorationsRef = useRef(null) const debounceTimerRef = useRef | null>(null) - const savedTimerRef = useRef | null>(null) const validationTimerRef = useRef | null>(null) const validationCounterRef = useRef(0) const contentCacheRef = useRef>(new Map()) @@ -314,12 +313,10 @@ export default function CodeEditor() { if (!configPath) return function finishSaving() { - setSaveStatus('saved') - if (savedTimerRef.current) clearTimeout(savedTimerRef.current) - savedTimerRef.current = setTimeout(() => setSaveStatus('idle'), SAVED_DISPLAY_DURATION) + setSaved() } - setSaveStatus('saving') + setSaving() if (isConfigurationFile(fileExtension ?? '')) { saveConfigurationFile(project.name, configPath, updatedContent) .then(({ xmlContent }) => { @@ -329,14 +326,14 @@ export default function CodeEditor() { }) .catch((error) => { showErrorToastFrom('Error saving', error) - setSaveStatus('idle') + setIdle() }) } else { updateFile(project.name, configPath, updatedContent) .then(() => finishSaving()) .catch((error) => { showErrorToastFrom('Error saving', error) - setSaveStatus('idle') + setIdle() }) } }, @@ -366,7 +363,6 @@ export default function CodeEditor() { useEffect(() => { return () => { if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current) - if (savedTimerRef.current) clearTimeout(savedTimerRef.current) if (validationTimerRef.current) clearTimeout(validationTimerRef.current) } }, []) @@ -486,6 +482,13 @@ export default function CodeEditor() { applyFlowHighlighter() + const updateButtonOffset = () => { + const layout = editor.getLayoutInfo() + setButtonRightOffset(layout.minimap.minimapWidth + layout.verticalScrollbarWidth + 8) + } + updateButtonOffset() + editor.onDidLayoutChange(updateButtonOffset) + editor.addAction({ id: 'save-file', label: 'Save File', @@ -703,31 +706,18 @@ export default function CodeEditor() { isDiffTab && activeTab.diffData ? ( ) : ( - <> -
    - - Path:{' '} - {activeTab.configurationPath && project - ? toProjectRelativePath(activeTab.configurationPath, project) - : activeTab.configurationPath} - -
    - +
    +
    +
    -
    -
    - +
    + +
    +
    ) ) : (
    diff --git a/src/main/frontend/app/routes/settings/pages/studio-settings.tsx b/src/main/frontend/app/routes/settings/pages/studio-settings.tsx index 58ebe17b..bb194cce 100644 --- a/src/main/frontend/app/routes/settings/pages/studio-settings.tsx +++ b/src/main/frontend/app/routes/settings/pages/studio-settings.tsx @@ -21,6 +21,20 @@ export default function StudioSettings() { onChange={(value: boolean) => setStudioSettings({ gradient: value })} /> + + + setStudioSettings({ paletteExpandedByDefault: value })} + /> +
    ) diff --git a/src/main/frontend/app/routes/studio/canvas/flow.config.ts b/src/main/frontend/app/routes/studio/canvas/flow.config.ts index ab6be257..fa3a82dc 100644 --- a/src/main/frontend/app/routes/studio/canvas/flow.config.ts +++ b/src/main/frontend/app/routes/studio/canvas/flow.config.ts @@ -1,6 +1,6 @@ export const FlowConfig = { NODE_DEFAULT_WIDTH: 300, - NODE_MIN_HEIGHT: 300, + NODE_MIN_HEIGHT: 80, EXIT_DEFAULT_WIDTH: 150, EXIT_DEFAULT_HEIGHT: 100, STICKY_NOTE_DEFAULT_WIDTH: 200, diff --git a/src/main/frontend/app/routes/studio/canvas/flow.tsx b/src/main/frontend/app/routes/studio/canvas/flow.tsx index 1fafa01a..7b3bbc33 100644 --- a/src/main/frontend/app/routes/studio/canvas/flow.tsx +++ b/src/main/frontend/app/routes/studio/canvas/flow.tsx @@ -14,6 +14,10 @@ import { useUpdateNodeInternals, } from '@xyflow/react' import Dagre from '@dagrejs/dagre' +import { SaveStatusIndicator } from '~/components/save-status-indicator' +import { useSaveStatusStore } from '~/stores/save-status-store' +import Button from '~/components/inputs/button' +import CodeIcon from '/icons/solar/Code.svg?react' import '@xyflow/react/dist/style.css' import FrankNodeComponent, { type FrankNodeType } from '~/routes/studio/canvas/nodetypes/frank-node' import FrankEdgeComponent from '~/routes/studio/canvas/edgetypes/frank-edge' @@ -43,7 +47,6 @@ import { refreshOpenDiffs } from '~/services/git-service' import useEditorTabStore from '~/stores/editor-tab-store' import { cloneWithRemappedIds, getEdgeLabelFromHandle } from '~/utils/flow-utils' import { showErrorToast } from '~/components/toast' -import clsx from 'clsx' import { useSettingsStore } from '~/stores/settings-store' import { useShortcut } from '~/hooks/use-shortcut' import CanvasContextMenu from '~/components/flow/canvas-context-menu' @@ -61,8 +64,6 @@ const selector = (state: FlowState) => ({ onReconnect: state.onReconnect, }) -type SaveStatus = 'idle' | 'saving' | 'saved' -const SAVED_DISPLAY_DURATION = 2000 const STICKY_SNAP_DISTANCE = 60 const getStickyCenter = (sticky: StickyNote) => ({ @@ -104,7 +105,7 @@ const nodeTypes = { } const edgeTypes = { frankEdge: FrankEdgeComponent } -function FlowCanvas() { +function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { const showNodeContextMenu = useNodeContextMenu() const [loading, setLoading] = useState(false) const { @@ -151,18 +152,19 @@ function FlowCanvas() { edges: Edge[] } | null>(null) - const [saveStatus, setSaveStatus] = useState('idle') + const { setSaving, setSaved, setIdle } = useSaveStatusStore() const [contextMenu, setContextMenu] = useState<{ x: number; y: number; flowPos: { x: number; y: number } } | null>( null, ) const autoSaveTimerRef = useRef | null>(null) - const savedTimerRef = useRef | null>(null) const isLoadingTabRef = useRef(false) const updateNodeInternals = useUpdateNodeInternals() const reactFlow = useReactFlow() const reactFlowRef = useRef(reactFlow) reactFlowRef.current = reactFlow + const canvasRef = useRef(null) + const fitAfterLayoutRef = useRef<{ id: string }[] | null>(null) const { nodes, edges, viewport, onNodesChange, onEdgesChange, onConnect, onReconnect } = useFlowStore( useShallow(selector), @@ -181,7 +183,7 @@ function FlowCanvas() { if (!configurationPath || !adapterName || !currentProject) return - setSaveStatus('saving') + setSaving() try { const fullConfigXml = await fetchConfigurationFileCached(currentProject.name, configurationPath) const configDoc = new DOMParser().parseFromString(fullConfigXml, 'text/xml') @@ -226,13 +228,11 @@ function FlowCanvas() { }) } - setSaveStatus('saved') - if (savedTimerRef.current) clearTimeout(savedTimerRef.current) - savedTimerRef.current = setTimeout(() => setSaveStatus('idle'), SAVED_DISPLAY_DURATION) + setSaved() } catch (error) { console.error('Failed to save XML:', error) showErrorToast(`Failed to save XML: ${error instanceof Error ? error.message : error}`) - setSaveStatus('idle') + setIdle() } }, [project]) @@ -251,7 +251,6 @@ function FlowCanvas() { useEffect(() => { return () => { if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current) - if (savedTimerRef.current) clearTimeout(savedTimerRef.current) } }, []) @@ -261,6 +260,15 @@ function FlowCanvas() { } }, [nodes, edges, scheduleAutoSave]) + useEffect(() => { + if (!fitAfterLayoutRef.current) return + const nodeIds = fitAfterLayoutRef.current + fitAfterLayoutRef.current = null + requestAnimationFrame(() => { + reactFlowRef.current?.fitView({ nodes: nodeIds, padding: 0.15, duration: 300 }) + }) + }, [nodes]) + useEffect(() => { useNodeContextStore.getState().registerSaveFlow(async () => { if (autoSaveTimerRef.current) { @@ -303,6 +311,43 @@ function FlowCanvas() { setShowModal(true) } + const computeFitViewport = useCallback((nodes: Node[]): { x: number; y: number; zoom: number } => { + const relevantNodes = nodes.filter((node) => node.type === 'frankNode' || node.type === 'exitNode') + if (relevantNodes.length === 0) return { x: 0, y: 0, zoom: 1 } + + const minX = Math.min(...relevantNodes.map((node) => node.position.x)) + const minY = Math.min(...relevantNodes.map((node) => node.position.y)) + const maxX = Math.max( + ...relevantNodes.map((node) => node.position.x + (node.measured?.width ?? FlowConfig.NODE_DEFAULT_WIDTH)), + ) + const maxY = Math.max( + ...relevantNodes.map((node) => node.position.y + (node.measured?.height ?? FlowConfig.NODE_MIN_HEIGHT)), + ) + + const deltaX = maxX - minX + const deltaY = maxY - minY + const centerX = minX + deltaX / 2 + const centerY = minY + deltaY / 2 + + const canvasWidth = canvasRef.current?.clientWidth ?? 800 + const canvasHeight = canvasRef.current?.clientHeight ?? 600 + + const padding = 0.85 + const zoom = Math.max( + 0.2, + Math.min( + 1.5, + Math.min((canvasWidth * padding) / Math.max(deltaX, 1), (canvasHeight * padding) / Math.max(deltaY, 1)), + ), + ) + + return { + x: canvasWidth / 2 - centerX * zoom, + y: canvasHeight / 2 - centerY * zoom - 40, + zoom, + } + }, []) + const layoutGraph = useCallback((nodes: Node[], edges: Edge[], direction: 'TB' | 'LR' = 'LR'): Node[] => { const dagreGraph = new Dagre.graphlib.Graph() dagreGraph.setDefaultEdgeLabel(() => ({})) @@ -312,47 +357,58 @@ function FlowCanvas() { nodesep: FlowConfig.LAYOUT_VERTICAL_OFFSET, }) - // Only add nodes to Dagre that need layout (position x=0 and y=0) + const layoutableIds = new Set() for (const node of nodes) { - if (node.position.x === 0 && node.position.y === 0) { - dagreGraph.setNode(node.id, { - width: node.width, - height: node.height, - }) + if ((node.type === 'frankNode' || node.type === 'exitNode') && node.position.x === 0 && node.position.y === 0) { + const width = node.measured?.width ?? FlowConfig.NODE_DEFAULT_WIDTH + const height = node.measured?.height ?? FlowConfig.NODE_MIN_HEIGHT + dagreGraph.setNode(node.id, { width: width, height: height }) + layoutableIds.add(node.id) } } - // Add all edges for (const edge of edges) { - dagreGraph.setEdge(edge.source, edge.target) + if (layoutableIds.has(edge.source) && layoutableIds.has(edge.target)) { + dagreGraph.setEdge(edge.source, edge.target) + } } Dagre.layout(dagreGraph) return nodes.map((node) => { - if (node.position.x !== 0 || node.position.y !== 0) return node + if (!layoutableIds.has(node.id)) return node const nodeWithPosition = dagreGraph.node(node.id) if (!nodeWithPosition) return node + const width = node.measured?.width ?? FlowConfig.NODE_DEFAULT_WIDTH + const height = node.measured?.height ?? FlowConfig.NODE_MIN_HEIGHT + return { ...node, position: { - x: nodeWithPosition.x, - y: nodeWithPosition.y, + x: nodeWithPosition.x - width / 2, + y: nodeWithPosition.y - height / 2, }, - - measured: node.measured, } }) }, []) const handleAutoLayout = useCallback(() => { const flowStore = useFlowStore.getState() - const resetNodes = flowStore.nodes.map((node) => ({ ...node, position: { x: 0, y: 0 } })) + const resetNodes = flowStore.nodes.map((node) => + node.type === 'frankNode' || node.type === 'exitNode' ? { ...node, position: { x: 0, y: 0 } } : node, + ) const laidOut = layoutGraph(resetNodes, flowStore.edges, 'LR') + + const nodeIds = laidOut + .filter((node) => node.type === 'frankNode' || node.type === 'exitNode') + .map((node) => ({ id: node.id })) + + if (nodeIds.length === 0) return + + fitAfterLayoutRef.current = nodeIds flowStore.setNodes(laidOut) - setTimeout(() => reactFlowRef.current.fitView({ padding: 0.1 }), 50) }, [layoutGraph]) const getFullySelectedGroupIds = useCallback( @@ -1030,9 +1086,9 @@ function FlowCanvas() { if (!adapter) return const adapterJson = await convertAdapterXmlToJson(adapter) flowStore.setEdges(adapterJson.edges) - flowStore.setViewport({ x: 0, y: 0, zoom: 1 }) const laidOutNodes = layoutGraph(adapterJson.nodes, adapterJson.edges, 'LR') flowStore.setNodes(laidOutNodes) + flowStore.setViewport(computeFitViewport(laidOutNodes)) flowStore.setHistory([]) flowStore.setFuture([]) } @@ -1095,7 +1151,7 @@ function FlowCanvas() { ) return () => unsubscribe() - }, [layoutGraph]) + }, [layoutGraph, computeFitViewport]) useEffect(() => { const unsub = useFlowStore.subscribe( @@ -1142,141 +1198,150 @@ function FlowCanvas() { return (
    - {loading && ( -
    -
    +
    +
    +
    - )} - - {isEditing && ( -
    -
    - - - Esc - {' '} - Discard - - | - - - Ctrl+Enter - {' '} - Save - + + {loading && ( +
    +
    -
    - )} - - { - useFlowStore.getState().setViewport(viewport) - }} - onNodesChange={onNodesChange} - onEdgesChange={onEdgesChange} - onConnect={onConnect} - onReconnect={onReconnect} - onNodeClick={handleNodeClick} - onNodeDoubleClick={handleNodeDoubleClick} - onNodeDragStop={handleNodeDragStop} - onEdgeClick={handleEdgeClick} - onSelectionChange={handleSelectionChange} - onConnectStart={handleConnectStart} - onConnectEnd={handleConnectEnd} - nodeTypes={nodeTypes} - edgeTypes={edgeTypes} - onPaneClick={() => { - setContextMenu(null) - setSelectedStickyId(null) - setSelectedGroupId(null) - if (!isDirty) { - showNodeContextMenu(false) - setIsEditing(false) - setParentId(null) - setChildParentId(null) - } - }} - deleteKeyCode={null} - minZoom={0.2} - > - - - - - - - - - - - - - - - - -
    - +
    + + + Esc + {' '} + Discard + + | + + + Ctrl+Enter + {' '} + Save + +
    +
    + )} + + { + useFlowStore.getState().setViewport(viewport) + }} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} + onConnect={onConnect} + onReconnect={onReconnect} + onNodeClick={handleNodeClick} + onNodeDoubleClick={handleNodeDoubleClick} + onNodeDragStop={handleNodeDragStop} + onEdgeClick={handleEdgeClick} + onSelectionChange={handleSelectionChange} + onConnectStart={handleConnectStart} + onConnectEnd={handleConnectEnd} + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + onPaneClick={() => { + setContextMenu(null) + setSelectedStickyId(null) + setSelectedGroupId(null) + if (!isDirty) { + showNodeContextMenu(false) + setIsEditing(false) + setParentId(null) + setChildParentId(null) + } + }} + deleteKeyCode={null} + minZoom={0.2} > - {saveStatus === 'saving' && 'Saving...'} - {saveStatus === 'saved' && 'Saved'} - + + + + + + + + + + + + + + + + + setShowModal(false)} + addNodeAtPosition={addNodeAtPosition} + positions={edgeDropPositions} + sourceInfo={sourceInfoReference.current} + /> + + {contextMenu && ( + setContextMenu(null)} + onAddNote={() => addStickyNote(contextMenu.flowPos)} + onGroup={handleGrouping} + onUngroup={handleUngroup} + onCut={cutSelection} + onCopy={copySelection} + onPaste={pasteSelection} + hasSelection={nodes.some((n) => n.selected)} + hasGroupedSelection={ + nodes.some((node) => node.selected) && allSelectedInSameGroup(nodes.filter((node) => node.selected)) + } + hasClipboard={clipboardRef.current !== null} + /> + )}
    - setShowModal(false)} - addNodeAtPosition={addNodeAtPosition} - positions={edgeDropPositions} - sourceInfo={sourceInfoReference.current} - /> - - {contextMenu && ( - setContextMenu(null)} - onAddNote={() => addStickyNote(contextMenu.flowPos)} - onGroup={handleGrouping} - onUngroup={handleUngroup} - onCut={cutSelection} - onCopy={copySelection} - onPaste={pasteSelection} - hasSelection={nodes.some((n) => n.selected)} - hasGroupedSelection={nodes.some((n) => n.selected) && allSelectedInSameGroup(nodes.filter((n) => n.selected))} - hasClipboard={clipboardRef.current !== null} - /> - )} +
    + +
    ) } -export default function Flow({ showNodeContextMenu }: Readonly<{ showNodeContextMenu: (b: boolean) => void }>) { +export default function Flow({ + showNodeContextMenu, + onOpenInEditor, +}: Readonly<{ showNodeContextMenu: (b: boolean) => void; onOpenInEditor: () => void }>) { return ( - + ) diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx index 0eb1731f..35642496 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx @@ -173,6 +173,16 @@ export default function FrankNode(properties: NodeProps) { } }, [properties.data.children, properties.data.sourceHandles.length, dragOver]) + useEffect(() => { + const container = containerReference.current + if (!container) return + const observer = new ResizeObserver(() => { + setIsOverflowing(container.scrollHeight > container.offsetHeight + 4) + }) + observer.observe(container) + return () => observer.disconnect() + }, []) + const addHandle = useFlowStore.getState().addHandle const addChild = useFlowStore((state) => state.addChild) @@ -447,7 +457,6 @@ export default function FrankNode(properties: NodeProps) { className={`bg-background border-border relative flex w-full flex-col items-center overflow-x-visible rounded-md border ${isManuallyResized ? 'h-full overflow-y-hidden' : 'overflow-y-visible'}`} style={{ minWidth: `${minNodeWidth}px`, - minHeight: `${minNodeHeight}px`, ...(properties.selected && { borderColor: `var(${colorVariable})` }), }} ref={containerReference} @@ -559,14 +568,12 @@ export default function FrankNode(properties: NodeProps) { {isOverflowing && (
    - ▼ more -
    + /> )}
    diff --git a/src/main/frontend/app/routes/studio/studio.tsx b/src/main/frontend/app/routes/studio/studio.tsx index 6fbae926..b3b81298 100644 --- a/src/main/frontend/app/routes/studio/studio.tsx +++ b/src/main/frontend/app/routes/studio/studio.tsx @@ -12,9 +12,6 @@ import { SidebarSide, useSidebarStore } from '~/components/sidebars-layout/sideb import SidebarLayout from '~/components/sidebars-layout/sidebar-layout' import useTabStore from '~/stores/tab-store' import { useShallow } from 'zustand/react/shallow' -import { useProjectStore } from '~/stores/project-store' -import { toProjectRelativePath } from '~/utils/path-utils' -import CodeIcon from '/icons/solar/Code.svg?react' import { openInEditor } from '~/actions/navigationActions' import Button from '~/components/inputs/button' import useFlowStore, { isStickyNote } from '~/stores/flow-store' @@ -148,7 +145,6 @@ function RightPanelContent({ } export default function Studio() { - const project = useProjectStore((state) => state.project) const setVisibility = useSidebarStore((state) => state.setVisibility) const [showNodeContext, setShowNodeContext] = useState(false) const { nodeId, editingSubtype, isMultiSelect, selectedStickyId, selectedGroupId } = useNodeContextStore( @@ -235,20 +231,7 @@ export default function Studio() { {activeTab ? ( <> -
    - - {activeTabPath && project ? toProjectRelativePath(activeTabPath, project) : activeTabPath} - - -
    - + ) : (
    diff --git a/src/main/frontend/app/stores/save-status-store.ts b/src/main/frontend/app/stores/save-status-store.ts new file mode 100644 index 00000000..15df0473 --- /dev/null +++ b/src/main/frontend/app/stores/save-status-store.ts @@ -0,0 +1,19 @@ +import { create } from 'zustand' + +export type SaveStatus = 'idle' | 'saving' | 'saved' + +interface SaveStatusState { + saveStatus: SaveStatus + savedAt: Date | null + setSaving: () => void + setSaved: () => void + setIdle: () => void +} + +export const useSaveStatusStore = create()((set) => ({ + saveStatus: 'idle', + savedAt: null, + setSaving: () => set({ saveStatus: 'saving' }), + setSaved: () => set({ saveStatus: 'saved', savedAt: new Date() }), + setIdle: () => set({ saveStatus: 'idle' }), +})) diff --git a/src/main/frontend/app/stores/settings-store.ts b/src/main/frontend/app/stores/settings-store.ts index 175124f3..73ff3206 100644 --- a/src/main/frontend/app/stores/settings-store.ts +++ b/src/main/frontend/app/stores/settings-store.ts @@ -65,7 +65,7 @@ const defaultStudioSettings: StudioSettings = { previewOnSave: true, autoRefresh: true, gradient: false, - paletteExpandedByDefault: true, + paletteExpandedByDefault: false, } const defaultProjectSettings: ProjectSettings = {