Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,30 @@ export default class EditorFilesDataProvider extends BaseFilesDataProvider<FileN
this.projectName = projectName
}

public async init() {
private expandedItems: string[] = []

public async init(expandedItems: string[] = []) {
this.expandedItems = expandedItems
await this.fetchAndBuildTree()
await this.preloadExpandedItems()
}

private async preloadExpandedItems() {
const sortedIds = [...this.expandedItems].toSorted((a, b) => a.split('/').length - b.split('/').length)
for (const id of sortedIds) {
if (id === 'root') continue
const item = this.data[id]
if (!item || !item.isFolder) continue
await this.loadDirectory(id)
}
}

public override async reloadDirectory(_itemId: TreeItemIndex): Promise<void> {
this.data = {}
this.loadedDirectories.clear()
await this.fetchAndBuildTree()
await this.preloadExpandedItems()
this.notifyListeners(Object.keys(this.data))
}

private async fetchAndBuildTree() {
Expand Down Expand Up @@ -86,7 +108,7 @@ export default class EditorFilesDataProvider extends BaseFilesDataProvider<FileN
children: isDirectory ? this.buildChildren(childIndex, child.children) : undefined,
}

if (isDirectory) {
if (isDirectory && child.children != null) {
this.loadedDirectories.add(child.path)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@

const initProvider = async () => {
const provider = new EditorFilesDataProvider(project.name)
await provider.init()
await provider.init(editorExpandedItems)

if (isMounted) {
setDataProvider(provider)
Expand All @@ -96,7 +96,7 @@
return () => {
isMounted = false
}
}, [project?.name])

Check warning on line 99 in src/main/frontend/app/components/file-structure/editor-file-structure.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

React Hook useEffect has a missing dependency: 'editorExpandedItems'. Either include it or remove the dependency array

useEffect(() => {
const findMatchingItems = async () => {
Expand Down Expand Up @@ -151,7 +151,6 @@
const item = await dataProvider.getTreeItem(itemId)
if (!item) return

// Toggle expanded state managed by onExpandItem naturally if needed
if (item.isFolder) {
return
}
Expand Down Expand Up @@ -213,14 +212,21 @@

const renderItemArrow = ({ item, context }: { item: TreeItem; context: TreeItemRenderContext }) => {
if (!item.isFolder) return null

const Icon = context.isExpanded ? AltArrowDownIcon : AltArrowRightIcon

const handleClick = (event: React.MouseEvent) => {
event.stopPropagation()
context.toggleExpandedState()
}

return <Icon onClick={handleClick} className="rct-tree-item-arrow-isFolder rct-tree-item-arrow fill-foreground" />
return (
<Icon
onClick={handleClick}
onContextMenu={(mouseEvent) => ctxMenu.openContextMenu(mouseEvent, item.index)}
className="rct-tree-item-arrow-isFolder rct-tree-item-arrow fill-foreground"
/>
)
}

const renderItemTitle = ({
Expand All @@ -238,6 +244,7 @@
const titleLower = title.toLowerCase()

let highlightedTitle: JSX.Element | string = title

if (searchTerm && titleLower.includes(searchLower)) {
const parts = title.split(new RegExp(`(${searchTerm})`, 'gi'))
highlightedTitle = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
return () => {
isMounted = false
}
}, [project])

Check warning on line 79 in src/main/frontend/app/components/file-structure/studio-file-structure.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

React Hook useEffect has a missing dependency: 'studioExpandedItems'. Either include it or remove the dependency array

useEffect(() => {
const findMatchingItems = async () => {
Expand Down Expand Up @@ -228,7 +228,11 @@
}

return (
<Icon onClick={handleArrowClick} className="rct-tree-item-arrow-isFolder rct-tree-item-arrow fill-foreground" />
<Icon
onClick={handleArrowClick}
onContextMenu={(mouseEvent: React.MouseEvent) => mouseEvent.stopPropagation()}
className="rct-tree-item-arrow-isFolder rct-tree-item-arrow fill-foreground"
/>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ export default class FilesDataProvider extends BaseFilesDataProvider<StudioItemD
}
}

public override async reloadDirectory(_itemId: TreeItemIndex): Promise<void> {
this.data = {}
this.loadedDirectories.clear()
await this.loadRoot()
this.notifyListeners(Object.keys(this.data))
}

private async loadRoot() {
const tree = await fetchProjectTree(this.projectName)

Expand Down Expand Up @@ -130,6 +137,24 @@ export default class FilesDataProvider extends BaseFilesDataProvider<StudioItemD
children: isFolder ? [] : undefined,
}

if (isFolder && child.name.endsWith('.xml') && child.adapterNames?.length) {
for (const [i, adapterName] of child.adapterNames.entries()) {
const adapterIndex = `${index}/${adapterName}::${i}`
this.data[adapterIndex] = {
index: adapterIndex,
data: { adapterName, configPath: child.path, adapterPosition: i },
isFolder: false,
}
this.data[index].children!.push(adapterIndex)
}
this.loadedDirectories.add(child.path)
} else if (child.type === 'DIRECTORY' && child.children != null) {
for (const subChild of sortChildren(child.children)) {
this.data[index].children!.push(this.buildChildItem(index, subChild))
}
this.loadedDirectories.add(child.path)
}

return index
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
renameInProject,
deleteInProject,
} from '~/services/file-tree-service'
import { clearConfigurationCache } from '~/services/configuration-service'
import useTabStore from '~/stores/tab-store'
import useEditorTabStore from '~/stores/editor-tab-store'
import { showErrorToastFrom } from '~/components/toast'

export interface ContextMenuState {
Expand Down Expand Up @@ -144,6 +147,11 @@ export function useFileTreeContextMenu({
}
try {
await renameInProject(projectName, oldPath, newName)
clearConfigurationCache(projectName, oldPath)
const lastSep = Math.max(oldPath.lastIndexOf('/'), oldPath.lastIndexOf('\\'))
const newPath = oldPath.slice(0, Math.max(0, lastSep + 1)) + newName
useTabStore.getState().renameTabsForConfig(oldPath, newPath)
useEditorTabStore.getState().refreshAllTabs()
const parentId = getParentItemId(itemId)
await dataProvider.reloadDirectory(parentId)
onAfterRename?.(oldPath, newName)
Expand Down Expand Up @@ -171,6 +179,9 @@ export function useFileTreeContextMenu({

try {
await deleteInProject(projectName, deleteTarget.path)
clearConfigurationCache(projectName, deleteTarget.path)
useTabStore.getState().removeTabsForConfig(deleteTarget.path)
useEditorTabStore.getState().refreshAllTabs()
onAfterDelete?.(deleteTarget.path)
await dataProvider.reloadDirectory(deleteTarget.parentItemId)
} catch (error) {
Expand Down
17 changes: 8 additions & 9 deletions src/main/frontend/app/routes/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,27 @@ import Navbar from '~/components/navbar/navbar'
import { FrankConfigXsdProvider } from '~/providers/frankconfig-xsd-provider'
import AppContent from '~/components/app-content'
import { useEffect, useState } from 'react'
import { useProjectStore, getStoredProjectName } from '~/stores/project-store'
import { fetchProject } from '~/services/project-service'
import { useProjectStore, getStoredProjectRootPath } from '~/stores/project-store'
import { openProject } from '~/services/project-service'
import LoadingSpinner from '~/components/loading-spinner'
import type { Project } from '~/types/project.types'
import { apiUrl } from '~/utils/api'

export default function AppLayout() {
const [restoring, setRestoring] = useState(!!getStoredProjectName())
const [restoring, setRestoring] = useState(!!getStoredProjectRootPath())

useEffect(() => {
const storedName = getStoredProjectName()
if (!storedName) {
const rootPath = getStoredProjectRootPath()
if (!rootPath) {
setRestoring(false)
return
}

fetchProject(storedName)
.then((fetched: Project) => {
openProject(rootPath)
.then((fetched) => {
useProjectStore.getState().setProject(fetched)
})
.catch(() => {
sessionStorage.removeItem('active-project-name')
useProjectStore.getState().clearProject()
})
.finally(() => {
setRestoring(false)
Expand Down
27 changes: 21 additions & 6 deletions src/main/frontend/app/routes/studio/canvas/flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import useNodeContextStore from '~/stores/node-context-store'
import CreateNodeModal from '~/components/flow/create-node-modal'
import { useProjectStore } from '~/stores/project-store'
import { clearConfigurationCache, fetchConfigurationCached, saveConfiguration } from '~/services/configuration-service'
import useEditorTabStore from '~/stores/editor-tab-store'
import { cloneWithRemappedIds } from '~/utils/flow-utils'
import { showErrorToast } from '~/components/toast'
import clsx from 'clsx'
Expand Down Expand Up @@ -106,18 +107,20 @@ function FlowCanvas() {
const project = useProjectStore.getState().project

const saveFlow = useCallback(async () => {
const flowData = reactFlowRef.current.toObject()
const { nodes: flowNodes, edges: flowEdges, viewport: flowViewport } = useFlowStore.getState()
const flowData = { nodes: flowNodes, edges: flowEdges, viewport: flowViewport }
const currentProject = useProjectStore.getState().project
const activeTabKey = useTabStore.getState().activeTab
const tabData = useTabStore.getState().getTab(activeTabKey)
const configurationPath = tabData?.configurationPath
const adapterName = tabData?.name
const adapterPosition = tabData?.adapterPosition

if (!configurationPath || !adapterName || !project) return
if (!configurationPath || !adapterName || !currentProject) return

setSaveStatus('saving')
try {
const fullConfigXml = await fetchConfigurationCached(project.name, configurationPath)
const fullConfigXml = await fetchConfigurationCached(currentProject.name, configurationPath)
const configDoc = new DOMParser().parseFromString(fullConfigXml, 'text/xml')
const allAdapters = [...configDoc.querySelectorAll('Adapter, adapter')]

Expand All @@ -134,7 +137,7 @@ function FlowCanvas() {

const newAdapterXml = await exportFlowToXml(
flowData,
project.name,
currentProject.name,
configurationPath,
adapterName,
existingAdapterXml,
Expand All @@ -148,8 +151,9 @@ function FlowCanvas() {

const updatedConfigXml = new XMLSerializer().serializeToString(configDoc).replace(/^<\?xml[^?]*\?>\s*/, '')

await saveConfiguration(project.name, configurationPath, updatedConfigXml)
clearConfigurationCache(project.name, configurationPath)
await saveConfiguration(currentProject.name, configurationPath, updatedConfigXml)
clearConfigurationCache(currentProject.name, configurationPath)
useEditorTabStore.getState().refreshAllTabs()

setSaveStatus('saved')
if (savedTimerRef.current) clearTimeout(savedTimerRef.current)
Expand Down Expand Up @@ -186,6 +190,17 @@ function FlowCanvas() {
}
}, [nodes, edges, scheduleAutoSave])

useEffect(() => {
useNodeContextStore.getState().registerSaveFlow(async () => {
if (autoSaveTimerRef.current) {
clearTimeout(autoSaveTimerRef.current)
autoSaveTimerRef.current = null
}
await saveFlow()
})
return () => useNodeContextStore.getState().registerSaveFlow(null)
}, [saveFlow])

const sourceInfoReference = useRef<{
nodeId: string | null
handleId: string | null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useFFDoc, useJavadocTransform} from "@frankframework/doc-library-react";
import { useFFDoc, useJavadocTransform } from '@frankframework/doc-library-react'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import type { ElementDetails } from '~/types/ff-doc.types'
Expand Down
4 changes: 4 additions & 0 deletions src/main/frontend/app/routes/studio/context/node-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,13 +232,15 @@ export default function NodeContext({
setShowNodeContext(false)
setParentId(null)
setChildParentId(null)
void useNodeContextStore.getState().saveFlow?.()
return
}
updateChild(parentNode.id, updatedChild)
setIsEditing(false)
setShowNodeContext(false)
setParentId(null)
setChildParentId(null)
void useNodeContextStore.getState().saveFlow?.()
return
}

Expand All @@ -250,6 +252,7 @@ export default function NodeContext({
setIsNewNode(false)
setIsEditing(false)
setShowNodeContext(false)
void useNodeContextStore.getState().saveFlow?.()
return
}
setAttributes(nodeId.toString(), newAttributesObject)
Expand All @@ -258,6 +261,7 @@ export default function NodeContext({
}
setIsEditing(false)
setShowNodeContext(false)
void useNodeContextStore.getState().saveFlow?.()
}

const canSaveRef = useRef(canSave)
Expand Down
6 changes: 6 additions & 0 deletions src/main/frontend/app/services/file-tree-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ export async function fetchProjectTree(projectName: string, signal?: AbortSignal
return apiFetch<FileTreeNode>(`/projects/${encodeURIComponent(projectName)}/tree/configurations`, { signal })
}

export async function fetchShallowConfigurationsTree(projectName: string, signal?: AbortSignal): Promise<FileTreeNode> {
return apiFetch<FileTreeNode>(`/projects/${encodeURIComponent(projectName)}/tree/configurations?shallow=true`, {
signal,
})
}

export async function fetchProjectRootTree(projectName: string, signal?: AbortSignal): Promise<FileTreeNode> {
return apiFetch<FileTreeNode>(`/projects/${encodeURIComponent(projectName)}/tree`, { signal })
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/frontend/app/stores/node-context-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface NodeContextStore {
childParentId: string | null
draggedName: string | null
editingSubtype: string | null
saveFlow: (() => Promise<void>) | null
setNodeId: (nodeId: number) => void
setAttributes: (attributes?: Record<string, Attribute>) => void
setIsEditing: (value: boolean) => void
Expand All @@ -21,6 +22,7 @@ interface NodeContextStore {
setChildParentId: (id: string | null) => void
setDraggedName: (name: string | null) => void
setEditingSubtype: (subtype: string | null) => void
registerSaveFlow: (fn: (() => Promise<void>) | null) => void
}

const useNodeContextStore = create<NodeContextStore>((set) => ({
Expand All @@ -33,6 +35,7 @@ const useNodeContextStore = create<NodeContextStore>((set) => ({
childParentId: null,
draggedName: null,
editingSubtype: null,
saveFlow: null,
setNodeId: (nodeId) => set({ nodeId }),
setAttributes: (attributes) => set({ attributes }),
setIsEditing: (value) => set({ isEditing: value }),
Expand All @@ -43,6 +46,7 @@ const useNodeContextStore = create<NodeContextStore>((set) => ({
setChildParentId: (childParentId: string | null) => set({ childParentId }),
setDraggedName: (draggedName) => set({ draggedName }),
setEditingSubtype: (editingSubtype) => set({ editingSubtype }),
registerSaveFlow: (saveFlow) => set({ saveFlow }),
}))

export default useNodeContextStore
Loading
Loading