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
72 changes: 61 additions & 11 deletions src/lib/components/tree/ConfigTree.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
} from "./../../../routes/Filter";
import {
selected_config,
selected_node_id,
selected_node_label,
show_supported_only,
hide_community_configs,
config_manager,
Expand Down Expand Up @@ -98,26 +100,33 @@
}

$: if (treeRoot) {
selectClosestMatch($selected_config, filteredConfigs);
const selected = get(selected_config);
// selectClosestMatch auto-selects a config when nothing at all is
// selected yet (startup, or the previous selection got deleted/filtered
// out). Skip it once a folder or nested preset is the active selection -
// those intentionally leave selected_config unset, and re-running this
// would keep clobbering that choice with configs[0].
if ($selected_node_id === undefined) {
selectClosestMatch($selected_config, filteredConfigs);
}
const currentNodeId = get(selected_node_id);

// A fresh selection should reveal its location, overriding any folder
// the user previously closed by hand. Re-selecting the same config
// the user previously closed by hand. Re-selecting the same node
// (e.g. toggling a folder it lives in) must not reset that choice.
if (selected?.id !== previousSelectedId) {
if (currentNodeId !== previousSelectedId) {
manuallyClosed.clear();
previousSelectedId = selected?.id;
previousSelectedId = currentNodeId;
}

// getIncludingNodes forces every ancestor of the selected config open so
// getIncludingNodes forces every ancestor of the selected node open so
// the selection stays visible. Drop any node the user explicitly closed
// (including a top-level category) so that choice sticks instead of
// snapping back open.
const includingNodes = treeRoot.getIncludingNodes(selected?.id);
const includingNodes = treeRoot.getIncludingNodes(currentNodeId);
const forcedNodes = includingNodes.filter((e) => !manuallyClosed.has(e));
treeProps = {
root: treeRoot,
selected: selected?.id,
selected: currentNodeId,
expanded: Array.from(new Set([...forcedNodes, ...manuallyExpanded])),
scrollBehaviour: {
scrollToIndex: scrollToSelectionTrigger > 0,
Expand Down Expand Up @@ -258,14 +267,36 @@
dragTarget.set(undefined);
}

// generateVirtualPresets (ConfigTree.ts) stamps a displayName ("Profile /
// Preset") on every synthetic preset/snippet node it fabricates for a
// profile's nested elements. Real, saved configs never set displayName, so
// it doubles as a reliable "this is a virtual node, not a loadable config"
// marker.
function isVirtualPreset(config: Config) {
return config.displayName !== undefined;
}

async function handleClick(node: AbstractTreeNode<any>) {
const config = (get(node).data as Tree.ItemData).item;
selected_config.set(config);
selected_node_id.set(config.id);
selected_node_label.set(config.displayName ?? config.name);

const isPreset = isVirtualPreset(config);
selected_config.set(isPreset ? undefined : config);

// provideSelectedConfigForEditor's handler (grid-editor side) switches on
// config.configType to pick the right module overlay - PRESET_LOAD for a
// preset, PROFILE_LOAD for a profile, or moduleOverlay.close() otherwise.
// It only updates that overlay + grid-editor's own selection store, it
// doesn't write anything to the device, so it's safe to send for a
// virtual/nested preset too even though there's nothing to preview here.
await parentIframeCommunication({
windowPostMessageName: "provideSelectedConfigForEditor",
dataForParent: { config: config },
});

if (isPreset) return;

await parentIframeCommunication({
windowPostMessageName: "showOverlay",
dataForParent: { value: true },
Expand All @@ -274,7 +305,23 @@

function handleContextMenu(node: AbstractTreeNode<any>) {
const config = (get(node).data as Tree.ItemData).item;
selected_config.set(config);
selected_node_id.set(config.id);
selected_node_label.set(config.displayName ?? config.name);
selected_config.set(isVirtualPreset(config) ? undefined : config);
}

function selectFolder(node: AbstractTreeNode<any>) {
const { title } = get(node).data as AbstractFolderData;
selected_node_id.set(get(node).id);
selected_node_label.set(title);
selected_config.set(undefined);
// config: undefined falls into provideSelectedConfigForEditor's default
// case (moduleOverlay.close()), so a folder selection clears any
// PRESET_LOAD/PROFILE_LOAD overlay left over from the previous selection.
parentIframeCommunication({
windowPostMessageName: "provideSelectedConfigForEditor",
dataForParent: { config: undefined },
});
}

// ── Inline rename ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -599,7 +646,10 @@
{item}
{level}
{expanded}
selected={get(item).id === $selected_node_id}
ctxOptions={getfolderCtxOptions(level, item)}
on:click={() => selectFolder(item)}
on:contextmenu={() => selectFolder(item)}
>
<span slot="title-label"
>{@html `${highlightMatches(data.title, $filter_value)} (${getItemCount(item)})`}</span
Expand All @@ -623,7 +673,7 @@
{itemProps}
{item}
compatible={data.compatible}
selected={data.item.id === $selected_config?.id}
selected={data.item.id === $selected_node_id}
{expanded}
editing={isRenaming}
ctxOptions={getItemCtxOptions(item)}
Expand Down
19 changes: 18 additions & 1 deletion src/lib/components/tree/TreeFolder.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@
TreeItemType,
} from "./TreeNode.svelte";
import { get } from "svelte/store";
import { createEventDispatcher } from "svelte";

const dispatch = createEventDispatcher();

export let item: AbstractTreeNode<any>;
export let expanded: boolean;
export let selected: boolean = false;
export let ctxOptions: ContextMenuOptions = { items: [] };
export let level: number;

Expand Down Expand Up @@ -43,7 +47,14 @@
$: data = $item.data as AbstractFolderData;
</script>

<div class="header" class:expanded use:contextTarget={ctxOptions}>
<div
class="header"
class:expanded
class:selected
use:contextTarget={ctxOptions}
on:click={() => dispatch("click")}
on:contextmenu={() => dispatch("contextmenu")}
>
<SvgIcon fill="var(--foreground-muted)" {iconPath} />
<div class="title">
<slot name="title-label">
Expand All @@ -61,6 +72,8 @@
width: 100%;
align-items: center;
padding: 0.25rem;
border-width: 1px;
border-color: transparent;
color: var(--foreground-muted);
}
.header:hover {
Expand All @@ -73,6 +86,10 @@
font-weight: bolder;
}

.selected {
border-color: #10b981;
}

.title {
display: flex;
flex-grow: 1;
Expand Down
3 changes: 2 additions & 1 deletion src/routes/ConfigCardDisplay.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
const dispatchEvent = createEventDispatcher();

export let data: Config | undefined;
export let emptyStateMessage: string = "No configuration is selected";

let configOwner: string = "";

Expand Down Expand Up @@ -188,7 +189,7 @@
style="color: var(--foreground-muted); background: var(--background)"
class="empty-state"
>
No configuration is selected
{emptyStateMessage}
</div>
{/if}
</div>
Expand Down
13 changes: 13 additions & 0 deletions src/routes/EditorLayout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import { filter_value, FilterValue } from "./Filter";
import {
selected_config,
selected_node_id,
selected_node_label,
hide_community_configs,
show_supported_only,
config_manager,
Expand Down Expand Up @@ -138,6 +140,10 @@

// Select the config and notify the editor
selected_config.set(matchedConfig);
selected_node_id.set(matchedConfig.id);
selected_node_label.set(
matchedConfig.displayName ?? matchedConfig.name,
);
scrollToSelectedConfigTrigger += 1;
await provideSelectedConfigForEditor(matchedConfig);
}
Expand Down Expand Up @@ -306,6 +312,8 @@
if (configs.length == 0) return;
configs.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
selected_config.set(configs[0]);
selected_node_id.set(configs[0].id);
selected_node_label.set(configs[0].displayName ?? configs[0].name);
scrollToSelectedConfigTrigger += 1; // trigger scrolling into view on ConfigTree
}

Expand All @@ -328,6 +336,8 @@
)[0];
if (newEntry) {
selected_config.set(newEntry);
selected_node_id.set(newEntry.id);
selected_node_label.set(newEntry.displayName ?? newEntry.name);
scrollToSelectedConfigTrigger += 1;
}
}
Expand Down Expand Up @@ -754,6 +764,9 @@
on:path-change={handlePathChange}
on:overwrite-profile={handleOverwriteProfile}
data={$selected_config}
emptyStateMessage={$selected_node_id !== undefined
? `No preview available for "${$selected_node_label}"`
: "No configuration is selected"}
>
<svelte:fragment slot="link-button">
{@const config = configs.find(
Expand Down
14 changes: 14 additions & 0 deletions src/routes/EditorLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ export const compatible_config_types: Writable<

export const selected_config: Writable<Config | undefined> = writable();

// Tracks which tree row is highlighted - a folder, a real top-level config,
// or a virtual preset/snippet nested under a profile. selected_config only
// ever holds a real, previewable config, so this is the only store that can
// represent "a folder/nested item is selected" for row highlighting and
// scroll-into-view purposes.
export const selected_node_id: Writable<string | undefined> = writable();

// Human-readable name for whatever selected_node_id points at (a folder's
// title, or a config's displayName/name), so the "no preview" placeholder
// can say what's selected instead of a generic message.
export const selected_node_label: Writable<string | undefined> = writable();

export function selectClosestMatch(
target: Config | undefined,
configs: Config[],
Expand All @@ -18,6 +30,8 @@ export function selectClosestMatch(
if (typeof current === "undefined") {
const first = configs[0];
selected_config.set(first);
selected_node_id.set(first?.id);
selected_node_label.set(first?.name);
}
}

Expand Down
Loading