Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/main/frontend/app/components/save-status-indicator.tsx
Original file line number Diff line number Diff line change
@@ -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 <span className="text-foreground-muted text-xs">☁️ Saving...</span>
if (saveStatus === 'saved' && savedAt)
return <span className="text-foreground-muted text-xs">☁️ Saved {getTimeAgo(savedAt)}</span>
return null
}
14 changes: 3 additions & 11 deletions src/main/frontend/app/components/tabs/tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<li
className={clsx(
'group border-r-border text-text relative flex h-full rotate-x-180 list-none items-center justify-between gap-1 border-r border-b px-4',
'group border-r-border text-text relative flex h-full rotate-x-180 list-none items-center justify-between gap-1 border-r border-b px-3',
isSelected
? 'border-t-brand border-b-background bg-background text-foreground hover:bg-background border-t-3 font-medium'
: 'bg-hover hover:bg-selected text-foreground-muted border-b-border border-t-3 border-t-transparent hover:cursor-pointer',
Expand All @@ -39,10 +31,10 @@ export default function Tab({ name, configurationPath, icon, isSelected, onSelec
title={configurationPath}
>
<Icon className={'fill-foreground-muted h-4 w-auto'} />
{displayText}
{name}
<CloseIcon
className={clsx(
'hover:fill-foreground h-8 w-auto hover:cursor-pointer',
'hover:fill-foreground h-5 w-auto hover:cursor-pointer',
isSelected ? 'fill-foreground-muted' : 'group-hover:fill-foreground-muted',
)}
onClick={handleClose}
Expand Down
45 changes: 15 additions & 30 deletions src/main/frontend/app/components/tabs/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,49 +10,34 @@ interface TabsViewProps<T extends string = string> {
}

export function TabsView<T extends string>({ tabs, activeTab, onSelectTab, onCloseTab }: TabsViewProps<T>) {
const tabsElementReference = useRef<HTMLDivElement>(null)
const tabsListReference = useRef<HTMLUListElement>(null)
const shadowLeftReference = useRef<HTMLDivElement>(null)
const shadowRightReference = useRef<HTMLDivElement>(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) {
Expand All @@ -64,7 +49,7 @@ export function TabsView<T extends string>({ tabs, activeTab, onSelectTab, onClo
}

return (
<div ref={tabsElementReference} className="relative flex h-12">
<div className="relative flex h-12">
<div
ref={shadowLeftReference}
className="absolute top-0 bottom-0 left-0 z-10 w-2 bg-gradient-to-r from-black/15 to-transparent opacity-0"
Expand Down
63 changes: 28 additions & 35 deletions src/main/frontend/app/routes/editor/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import SidebarLayout from '~/components/sidebars-layout/sidebar-layout'
import { SidebarSide } from '~/components/sidebars-layout/sidebar-layout-store'
import EditorTabs from '~/components/tabs/editor-tabs'
import { SaveStatusIndicator } from '~/components/save-status-indicator'
import { useSaveStatusStore } from '~/stores/save-status-store'
import { showErrorToastFrom } from '~/components/toast'
import { useTheme } from '~/hooks/use-theme'
import { fetchConfigurationFile, saveConfigurationFile } from '~/services/configuration-file-service'
Expand All @@ -30,7 +32,6 @@
import useEditorTabStore from '~/stores/editor-tab-store'
import { useProjectStore } from '~/stores/project-store'
import { useSettingsStore } from '~/stores/settings-store'
import { toProjectRelativePath } from '~/utils/path-utils'
import flowXsd from '../../../src/assets/xsd/FlowConfig.xsd?raw'
import {
extractFlowElements,
Expand All @@ -42,7 +43,6 @@
} from './xml-utils'

type LeftTab = 'files' | 'git'
type SaveStatus = 'idle' | 'saving' | 'saved'
export interface ValidationError {
message: string
lineNumber: number
Expand All @@ -61,7 +61,6 @@
type: string
}

const SAVED_DISPLAY_DURATION = 2000
const ELEMENT_ERROR_RE = /[Ee]lement [\u2018\u2019'"{]?([\w:.-]+)[\u2018\u2019'"}]?/
const ATTRIBUTE_ERROR_RE = /[Aa]ttribute [\u2018\u2019'"{]?([\w:.-]+)[\u2018\u2019'"}]?/

Expand Down Expand Up @@ -246,17 +245,17 @@
const [activeTabFilePath, setActiveTabFilePath] = useState<string>(useEditorTabStore.getState().activeTabFilePath)
const [fileContent, setFileContent] = useState<string>('')
const [fileLanguage, setFileLanguage] = useState<string>('xml')
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle')
const { setSaving, setSaved, setIdle } = useSaveStatusStore()
const [leftTab, setLeftTab] = useState<LeftTab>('files')
const [editorMounted, setEditorMounted] = useState(false)
const [xsdLoaded, setXsdLoaded] = useState(false)
const [buttonRightOffset, setButtonRightOffset] = useState(14)
const editorReference = useRef<Parameters<OnMount>[0] | null>(null)
const monacoReference = useRef<Monaco | null>(null)
const xsdContentRef = useRef<string | null>(null)
const errorDecorationsRef = useRef<{ clear: () => void } | null>(null)
const flowDecorationsRef = useRef<IEditorDecorationsCollection | null>(null)
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const savedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const validationTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const validationCounterRef = useRef(0)
const contentCacheRef = useRef<Map<string, CachedFile>>(new Map())
Expand Down Expand Up @@ -314,12 +313,10 @@
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 }) => {
Expand All @@ -329,18 +326,18 @@
})
.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()
})
}
},
[project, activeTabFilePath, isDiffTab],

Check warning on line 340 in src/main/frontend/app/routes/editor/editor.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

React Hook useCallback has missing dependencies: 'setIdle', 'setSaved', and 'setSaving'. Either include them or remove the dependency array
)

const flushPendingSave = useCallback(() => {
Expand All @@ -366,7 +363,6 @@
useEffect(() => {
return () => {
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
if (savedTimerRef.current) clearTimeout(savedTimerRef.current)
if (validationTimerRef.current) clearTimeout(validationTimerRef.current)
}
}, [])
Expand Down Expand Up @@ -486,6 +482,13 @@

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',
Expand Down Expand Up @@ -703,31 +706,18 @@
isDiffTab && activeTab.diffData ? (
<DiffTabView diffData={activeTab.diffData} />
) : (
<>
<div className="border-b-border bg-background flex h-12 items-center justify-between border-b p-4">
<span>
Path:{' '}
{activeTab.configurationPath && project
? toProjectRelativePath(activeTab.configurationPath, project)
: activeTab.configurationPath}
</span>
<div className="flex items-center gap-2">
<span
className={clsx(
'text-muted-foreground text-xs transition-opacity duration-300',
saveStatus === 'idle' ? 'opacity-0' : 'opacity-100',
)}
<div className="flex h-full flex-col">
<div className="relative min-h-0 flex-1">
<div className="absolute top-2 z-10" style={{ right: buttonRightOffset }}>
<Button
onClick={handleOpenInStudio}
className="flex items-center gap-1.5 text-xs shadow-sm"
title="Open in Studio"
>
{saveStatus === 'saving' && 'Saving...'}
{saveStatus === 'saved' && 'Saved'}
</span>
<Button onClick={handleOpenInStudio} className="flex items-center gap-1.5" title="Open in Studio">
<RulerCrossPenIcon className="h-4 w-4 fill-current" />
<RulerCrossPenIcon className="h-3.5 w-3.5 fill-current" />
Open in Studio
</Button>
</div>
</div>
<div className="h-full">
<Editor
language={fileLanguage}
theme={theme === 'dark' ? 'vs-dark' : 'vs'}
Expand All @@ -737,7 +727,7 @@
scheduleSave()
if (value && fileLanguage === 'xml') {
scheduleSchemaValidation(value)
applyFlowHighlighter() // Real-time highlight updates
applyFlowHighlighter()
}
}}
options={{
Expand All @@ -749,7 +739,10 @@
}}
/>
</div>
</>
<div className="border-t-border bg-background flex h-7 shrink-0 items-center justify-end border-t px-3">
<SaveStatusIndicator />
</div>
</div>
)
) : (
<div className="text-foreground-muted flex h-full flex-col items-center justify-center p-8 text-center">
Expand Down
14 changes: 14 additions & 0 deletions src/main/frontend/app/routes/settings/pages/studio-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ export default function StudioSettings() {
onChange={(value: boolean) => setStudioSettings({ gradient: value })}
/>
</InputWithLabel>

<InputWithLabel
inputSide="right"
grow
htmlFor="palette-expanded-toggle"
label="Expand Palette Categories"
description="Show all palette categories expanded by default"
>
<Toggle
id="palette-expanded-toggle"
checked={studio.paletteExpandedByDefault}
onChange={(value: boolean) => setStudioSettings({ paletteExpandedByDefault: value })}
/>
</InputWithLabel>
</div>
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion src/main/frontend/app/routes/studio/canvas/flow.config.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading
Loading