From 439f56d94d6e158738a2ad5ec507bb88e31bf06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1sztor=20D=C3=A1niel?= Date: Sun, 28 Jun 2026 18:54:51 +0200 Subject: [PATCH 01/11] Jump to linked config --- src/lib/components/tree/ConfigTree.svelte | 49 ++++++++++++-------- src/lib/components/tree/ConfigTree.ts | 13 +++--- src/lib/components/tree/TreeComponent.svelte | 34 ++++++++++---- src/lib/configmanager/ConfigManager.ts | 30 ++---------- src/routes/EditorLayout.svelte | 39 +++++++++++++--- 5 files changed, 100 insertions(+), 65 deletions(-) diff --git a/src/lib/components/tree/ConfigTree.svelte b/src/lib/components/tree/ConfigTree.svelte index e3c2536..1c5bb8f 100644 --- a/src/lib/components/tree/ConfigTree.svelte +++ b/src/lib/components/tree/ConfigTree.svelte @@ -15,19 +15,16 @@ compatible_config_types, selectClosestMatch, } from "./../../../routes/EditorLayout"; - import { type Config } from "../../schemas"; + import type { Config } from "../../schemas"; import { Tree } from "./ConfigTree"; import { createEventDispatcher } from "svelte"; import { parentIframeCommunication } from "../../utils"; import { dragTarget } from "../../actions/drag.action"; import ProfileCloudTreeItem from "./ProfileCloudTreeItem.svelte"; - import { - type AbstractFolderData, - AbstractTreeNode, - TreeItemType, - type ContextMenuOptions, - type TreeProperties, - } from "./TreeNode.svelte"; + import { TreeItemType } from "./TreeNode.svelte"; + import type { AbstractFolderData, AbstractTreeNode } from "./TreeNode.svelte"; + import type { TreeProperties } from "./TreeComponent.svelte"; + import type { ContextMenuOptions } from "@intechstudio/grid-uikit"; import TreeFolder from "./TreeFolder.svelte"; @@ -36,8 +33,11 @@ const dispatch = createEventDispatcher(); export let configs: Config[]; + export let scrollToSelectionTrigger = 0; let treeProps: TreeProperties; + let treeRoot: Tree.Node; + let filteredConfigs: Config[] = []; export function debounced(store: Writable, delay = 300) { let timeout: ReturnType; @@ -56,7 +56,7 @@ export const debounced_filter_value = debounced(filter_value, 300); $: { - treeProps = buildProps( + const tree = buildTree( configs, $debounced_filter_value, $sort_key, @@ -64,6 +64,24 @@ $hide_community_configs, $compatible_config_types, ); + treeRoot = tree.root; + filteredConfigs = tree.filteredConfigs; + } + + $: if (treeRoot) { + selectClosestMatch($selected_config, filteredConfigs); + const selected = get(selected_config); + + treeProps = { + root: treeRoot, + selected: selected?.id, + expanded: treeRoot.getIncludingNodes(selected?.id), + scrollBehaviour: { + scrollToIndex: scrollToSelectionTrigger > 0, + scrollTrigger: scrollToSelectionTrigger, + easing: "smooth", + }, + }; } function filterNode(node: Tree.Node, filter: FilterValue, level = 0) { @@ -112,14 +130,14 @@ }); } - function buildProps( + function buildTree( configs: Config[], filter: FilterValue, key: Sort.Key, supported: boolean, community: boolean, compatibileTypes: string[], - ): TreeProperties { + ) { const filteredConfigs = configs.filter((e) => matches(e, filter)); const root = Tree.create(configs, { showSupportedOnly: supported, @@ -129,14 +147,7 @@ filterNode(root, filter); - selectClosestMatch($selected_config, filteredConfigs); - - return { - root: root, - selected: $selected_config?.id, - expanded: root.getIncludingNodes($selected_config?.id), - scrollBehaviour: { scrollToIndex: true, easing: "instant" }, - }; + return { root, filteredConfigs }; } function handleDeleteVirtualDirectory(title: string) { diff --git a/src/lib/components/tree/ConfigTree.ts b/src/lib/components/tree/ConfigTree.ts index a51b18e..f9e1d87 100644 --- a/src/lib/components/tree/ConfigTree.ts +++ b/src/lib/components/tree/ConfigTree.ts @@ -4,7 +4,7 @@ import { config_manager, compatible_config_types, } from "./../../../routes/EditorLayout"; -import { type Config } from "../../schemas"; +import type { Config } from "../../schemas"; import { filter_value } from "../../../routes/Filter"; import { Sort } from "../../../routes/Sorter"; @@ -22,6 +22,7 @@ import { ElementType, grid, ModuleType } from "@intechstudio/grid-protocol"; export namespace Tree { export interface Options { showSupportedOnly?: boolean; + hideCommunityConfigs?: boolean; compatibileTypes: string[]; } @@ -287,7 +288,7 @@ export namespace Tree { e.owner ?? "", ); - const cct = get(compatible_config_types); + const cct = get(compatible_config_types) as string[]; return ( !isMyConfig && @@ -311,7 +312,7 @@ export namespace Tree { const isOfficialConfig = configuration.WORKFLOW_CONFIG_PROFILE_IDS.includes(e.owner ?? ""); - const cct = get(compatible_config_types); + const cct = get(compatible_config_types) as string[]; return ( !isMyConfig && @@ -336,7 +337,7 @@ export namespace Tree { configuration.RECOMMENDED_CONFIG_PROFILE_IDS.includes( e.owner ?? "", ); - const cct = get(compatible_config_types); + const cct = get(compatible_config_types) as string[]; return ( !isMyConfig && !isOfficialConfig && @@ -356,7 +357,7 @@ export namespace Tree { .filter((e: Config) => { const isMyConfig = e.syncStatus == "local" || e.owner === cm?.getCurrentOwnerId(); - const cct = get(compatible_config_types); + const cct = get(compatible_config_types) as string[]; return !isMyConfig && (!showSupportedOnly || cct.includes(e.type)); }) .map( @@ -372,7 +373,7 @@ export namespace Tree { .filter((e: Config) => { const isMyConfig = e.syncStatus == "local" || e.owner === cm?.getCurrentOwnerId(); - const cct = get(compatible_config_types); + const cct = get(compatible_config_types) as string[]; return !isMyConfig && showSupportedOnly && !cct.includes(e.type); }) diff --git a/src/lib/components/tree/TreeComponent.svelte b/src/lib/components/tree/TreeComponent.svelte index b786a79..7b3821f 100644 --- a/src/lib/components/tree/TreeComponent.svelte +++ b/src/lib/components/tree/TreeComponent.svelte @@ -2,6 +2,7 @@ export interface TreeScrollBehaviour { scrollToIndex?: boolean; easing?: "smooth" | "instant"; + scrollTrigger?: number; } export interface TreeProperties { @@ -13,10 +14,12 @@ diff --git a/src/lib/configmanager/ConfigManager.ts b/src/lib/configmanager/ConfigManager.ts index 907e032..3e9dafb 100644 --- a/src/lib/configmanager/ConfigManager.ts +++ b/src/lib/configmanager/ConfigManager.ts @@ -6,12 +6,10 @@ import { where, onSnapshot, doc, - getDoc, deleteDoc, setDoc, updateDoc, } from "firebase/firestore"; -import { v4 as uuidv4 } from "uuid"; import { configsCollection } from "../collections"; import { type CloudConfig, @@ -27,7 +25,7 @@ export interface ConfigManager { cancel(): void; deleteConfig(config: Config): Promise; saveConfig(config: BaseConfig, createMissingConfigs: boolean): Promise; - importLinkedConfig(linkId: string): Promise; + findLinkedConfigAppId(linkId: string): Promise; changeCloudVisibility(config: Config, visibility: boolean): Promise; getCurrentOwnerId(): string | null | undefined; getConfigCloudId(config: Config): string | undefined; @@ -320,28 +318,8 @@ export function createConfigManager(observer: { ); } - async function importLinkedConfig(linkId: string) { - const docRef = doc(configsCollection, linkId); - let configLink = await getDoc(docRef) - .then((res) => CloudConfigSchema.parse(res.data())) - .catch((err) => { - parentIframeCommunication({ - windowPostMessageName: "sendLogMessage", - dataForParent: { - type: "fail", - message: "Error importing config link!", - }, - }); - return undefined; - }); - - if (configLink) { - configLink.name = `Copy of ${configLink.name}`; - configLink.owner = undefined; - configLink.id = uuidv4(); - await saveConfig(configLink, true); - } - return configLink; + async function findLinkedConfigAppId(linkId: string) { + return configIdToAppConfigIdMap.get(linkId); } function getCurrentOwnerId() { @@ -356,7 +334,7 @@ export function createConfigManager(observer: { cancel, deleteConfig, saveConfig, - importLinkedConfig, + findLinkedConfigAppId, changeCloudVisibility, getCurrentOwnerId, getConfigCloudId, diff --git a/src/routes/EditorLayout.svelte b/src/routes/EditorLayout.svelte index e06b647..cdf103a 100644 --- a/src/routes/EditorLayout.svelte +++ b/src/routes/EditorLayout.svelte @@ -52,11 +52,13 @@ import { last } from "@melt-ui/svelte/internal/helpers"; let configs: Config[] = []; + let scrollToSelectedConfigTrigger = 0; let linkFlag: string | undefined = undefined; let usernameInput = { element: null as HTMLInputElement | null, + value: null, exists: false, valid: false, active: false, @@ -96,15 +98,33 @@ case "configLink": { const cm = get(config_manager); - const linkedConfig = await cm?.importLinkedConfig( + const linkedConfigAppId = await cm?.findLinkedConfigAppId( event.data.configLinkId, ); - if (linkedConfig) { + if (linkedConfigAppId) { + // Find the merged config from our configs list that matches the linked config ID + const matchedConfig = configs.find((c) => c.id === linkedConfigAppId); + + if (matchedConfig) { + // Select the config and notify the editor + selected_config.set(matchedConfig); + scrollToSelectedConfigTrigger += 1; + await provideSelectedConfigForEditor(matchedConfig); + } + submitAnalytics({ eventName: "Cloud Action", payload: { - click: "Config Link Import", + click: "Config Link Navigate", + }, + }); + } else { + parentIframeCommunication({ + windowPostMessageName: "sendLogMessage", + dataForParent: { + type: "fail", + message: "Config not found or no longer available.", }, }); } @@ -433,7 +453,7 @@ $: handleSelectionChange($selected_config); - function handleSelectionChange(config) { + function handleSelectionChange(config: Config | undefined) { if (lastid === config?.id) { return; } @@ -446,7 +466,11 @@ updateVisiblility(publicToggleValue); } - function updateVisiblility(value) { + function updateVisiblility(value: boolean | undefined) { + if (typeof value === "undefined") { + return; + } + const config = configs.find((e) => e.id === $selected_config?.id); if (typeof config === "undefined") { return; @@ -511,7 +535,10 @@ >
- +
From 2324ae1d4221ca436185aeb33161e04df550c5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20Kerti?= <47832952+kkerti@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:40:43 +0200 Subject: [PATCH 02/11] kk discovery, make it work, but tree is rebuilt every time and config jumps to bottom --- package-lock.json | 42 -------------------- src/lib/components/tree/ConfigTree.svelte | 3 +- src/lib/components/tree/TreeComponent.svelte | 25 ++++++++++-- src/routes/EditorLayout.svelte | 7 +++- 4 files changed, 29 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 545122e..d8c305c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3263,18 +3263,6 @@ "dev": true, "license": "ISC" }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -4240,21 +4228,6 @@ } } }, - "node_modules/svelte-check/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/svelte-check/node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -4825,21 +4798,6 @@ "node": ">=10" } }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/src/lib/components/tree/ConfigTree.svelte b/src/lib/components/tree/ConfigTree.svelte index 1c5bb8f..f460836 100644 --- a/src/lib/components/tree/ConfigTree.svelte +++ b/src/lib/components/tree/ConfigTree.svelte @@ -17,7 +17,7 @@ } from "./../../../routes/EditorLayout"; import type { Config } from "../../schemas"; import { Tree } from "./ConfigTree"; - import { createEventDispatcher } from "svelte"; + import { createEventDispatcher, tick } from "svelte"; import { parentIframeCommunication } from "../../utils"; import { dragTarget } from "../../actions/drag.action"; import ProfileCloudTreeItem from "./ProfileCloudTreeItem.svelte"; @@ -71,6 +71,7 @@ $: if (treeRoot) { selectClosestMatch($selected_config, filteredConfigs); const selected = get(selected_config); + console.log("we're triggered here", selected); treeProps = { root: treeRoot, diff --git a/src/lib/components/tree/TreeComponent.svelte b/src/lib/components/tree/TreeComponent.svelte index 7b3821f..55f3f92 100644 --- a/src/lib/components/tree/TreeComponent.svelte +++ b/src/lib/components/tree/TreeComponent.svelte @@ -67,7 +67,12 @@ let scrollTimeout: any; let lastScrollTrigger: number | undefined; - async function scrollToNode(id: string, attempt = 0) { + async function scrollToNode( + id: string, + attempt = 0, + block: ScrollLogicalPosition = "center", + behavior: ScrollBehavior = scrollBehaviour.easing ?? "smooth", + ) { clearTimeout(scrollTimeout); await tick(); scrollTimeout = setTimeout(() => { @@ -76,15 +81,15 @@ ) as HTMLElement; if (target) { target.scrollIntoView({ - behavior: scrollBehaviour.easing ?? "smooth", - block: "center", + behavior, + block, inline: "nearest", }); return; } if (attempt < 4) { - scrollToNode(id, attempt + 1); + scrollToNode(id, attempt + 1, block, behavior); } }, 100); } @@ -99,6 +104,18 @@ lastScrollTrigger = scrollBehaviour.scrollTrigger; scrollToNode(selected); } + + // When the tree is re-rendered (e.g. because treeRoot rebuilt due to reactive + // dependency changes after a click), restore the selected item into view. + // Using block:"nearest" avoids any visible scroll if it's already in viewport. + let prevTree: typeof tree; + $: if (tree !== prevTree) { + const wasRendered = prevTree !== undefined; + prevTree = tree; + if (wasRendered && selected) { + scrollToNode(selected, 0, "nearest", "instant"); + } + } {#key tree} diff --git a/src/routes/EditorLayout.svelte b/src/routes/EditorLayout.svelte index d3983e0..6e2f0b5 100644 --- a/src/routes/EditorLayout.svelte +++ b/src/routes/EditorLayout.svelte @@ -102,6 +102,8 @@ event.data.configLinkId, ); + console.log("[PC] linkedConfigAppId", linkedConfigAppId); + if (linkedConfigAppId) { // Find the merged config from our configs list that matches the linked config ID const matchedConfig = configs.find((c) => c.id === linkedConfigAppId); @@ -531,7 +533,10 @@
- +
From b3eea8daf05e9e5e990edba6b4059d665a030ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20Kerti?= <47832952+kkerti@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:50:11 +0200 Subject: [PATCH 03/11] kk when community configs hidden checkbox is changed, items might be null --- src/lib/components/tree/TreeNode.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/tree/TreeNode.svelte b/src/lib/components/tree/TreeNode.svelte index 207bed1..3ce5fb6 100644 --- a/src/lib/components/tree/TreeNode.svelte +++ b/src/lib/components/tree/TreeNode.svelte @@ -173,7 +173,7 @@ } $: { - const items = Array.from(listItems.values()); + const items = Array.from(listItems.values()).filter(Boolean); if (items.length > 0 && level === 0) { setMaxHeight(rootHeight, items); } From 82a16e8acfb922e7d6085c21da4a17a640b09029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20Kerti?= <47832952+kkerti@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:51:15 +0200 Subject: [PATCH 04/11] kk guard against tree rebuilds when ctx is already present --- src/lib/components/tree/TreeComponent.svelte | 89 ++++++++------------ 1 file changed, 35 insertions(+), 54 deletions(-) diff --git a/src/lib/components/tree/TreeComponent.svelte b/src/lib/components/tree/TreeComponent.svelte index 55f3f92..19f5b74 100644 --- a/src/lib/components/tree/TreeComponent.svelte +++ b/src/lib/components/tree/TreeComponent.svelte @@ -36,7 +36,7 @@ let rootHeight: number; let ctx: TreeView; - $: if ($root?.id) { + $: if ($root?.id && !ctx) { ctx = createTreeView({ onExpandedChange: handleExpandedChange }); setContext("tree", ctx); } @@ -67,12 +67,7 @@ let scrollTimeout: any; let lastScrollTrigger: number | undefined; - async function scrollToNode( - id: string, - attempt = 0, - block: ScrollLogicalPosition = "center", - behavior: ScrollBehavior = scrollBehaviour.easing ?? "smooth", - ) { + async function scrollToNode(id: string, attempt = 0) { clearTimeout(scrollTimeout); await tick(); scrollTimeout = setTimeout(() => { @@ -81,15 +76,15 @@ ) as HTMLElement; if (target) { target.scrollIntoView({ - behavior, - block, + behavior: scrollBehaviour.easing ?? "smooth", + block: "center", inline: "nearest", }); return; } if (attempt < 4) { - scrollToNode(id, attempt + 1, block, behavior); + scrollToNode(id, attempt + 1); } }, 100); } @@ -104,52 +99,38 @@ lastScrollTrigger = scrollBehaviour.scrollTrigger; scrollToNode(selected); } - - // When the tree is re-rendered (e.g. because treeRoot rebuilt due to reactive - // dependency changes after a click), restore the selected item into view. - // Using block:"nearest" avoids any visible scroll if it's already in viewport. - let prevTree: typeof tree; - $: if (tree !== prevTree) { - const wasRendered = prevTree !== undefined; - prevTree = tree; - if (wasRendered && selected) { - scrollToNode(selected, 0, "nearest", "instant"); - } - } -{#key tree} -
    - - - - - - - - - -
-{/key} +
    + + + + + + + + + +