@industream/flowmaker-flowbox-ui-components 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/dist/DCCatalogEntryPicker/AssetTree.svelte +0 -180
- package/dist/DCCatalogEntryPicker/AssetTree.svelte.d.ts +0 -13
- package/dist/DCCatalogEntryPicker/LabelFacet.svelte +0 -89
- package/dist/DCCatalogEntryPicker/LabelFacet.svelte.d.ts +0 -15
- package/dist/DCCatalogEntryPicker/picker-tree.d.ts +0 -20
- package/dist/DCCatalogEntryPicker/picker-tree.js +0 -52
- package/dist/DCCatalogEntryPicker/picker-tree.test.d.ts +0 -1
- package/dist/DCCatalogEntryPicker/picker-tree.test.js +0 -54
- package/src/DCCatalogEntryPicker/AssetTree.svelte +0 -180
- package/src/DCCatalogEntryPicker/LabelFacet.svelte +0 -89
- package/src/DCCatalogEntryPicker/picker-tree.test.ts +0 -70
- package/src/DCCatalogEntryPicker/picker-tree.ts +0 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@industream/flowmaker-flowbox-ui-components",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Reusable Svelte components for FlowMaker FlowBox UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./dist/index.js",
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"svelte": "^5.0.0",
|
|
48
|
-
"@industream/datacatalog-client": "1.9.1"
|
|
48
|
+
"@industream/datacatalog-client": "^1.9.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@industream/datacatalog-client": "1.9.
|
|
51
|
+
"@industream/datacatalog-client": "1.9.2",
|
|
52
52
|
"svelte": "^5.0.0",
|
|
53
53
|
"vite": "^6.0.0",
|
|
54
54
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
/**
|
|
3
|
-
* AssetTree — loads an asset dictionary as a tree and lets the user pick a node.
|
|
4
|
-
* Emits the selected node's own entryIds to the container, which fetches and
|
|
5
|
-
* filters the entries. This component holds no selection or entry state.
|
|
6
|
-
*/
|
|
7
|
-
import { firstDictionaryId, collectEntryIds } from './picker-tree';
|
|
8
|
-
|
|
9
|
-
let {
|
|
10
|
-
client = null,
|
|
11
|
-
onSelectNode = null
|
|
12
|
-
} = $props();
|
|
13
|
-
|
|
14
|
-
let dictionaries = $state([]);
|
|
15
|
-
let activeDictionaryId = $state('');
|
|
16
|
-
let rootNodes = $state([]);
|
|
17
|
-
let expanded = $state(new Set());
|
|
18
|
-
let selectedNodeId = $state('');
|
|
19
|
-
let loading = $state(false);
|
|
20
|
-
let error = $state('');
|
|
21
|
-
|
|
22
|
-
/** Load the dictionary list when a client becomes available. */
|
|
23
|
-
$effect(() => {
|
|
24
|
-
if (client) loadDictionaries(client);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
async function loadDictionaries(dc) {
|
|
28
|
-
loading = true;
|
|
29
|
-
error = '';
|
|
30
|
-
try {
|
|
31
|
-
const result = await dc.assetDictionaries.get({ asTree: true });
|
|
32
|
-
dictionaries = result.items ?? [];
|
|
33
|
-
const first = firstDictionaryId(dictionaries);
|
|
34
|
-
if (first) {
|
|
35
|
-
activeDictionaryId = first;
|
|
36
|
-
await loadTree(dc, first);
|
|
37
|
-
} else {
|
|
38
|
-
rootNodes = [];
|
|
39
|
-
}
|
|
40
|
-
} catch (err) {
|
|
41
|
-
error = err?.message ?? 'Failed to load asset dictionaries';
|
|
42
|
-
dictionaries = [];
|
|
43
|
-
rootNodes = [];
|
|
44
|
-
} finally {
|
|
45
|
-
loading = false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function loadTree(dc, id) {
|
|
50
|
-
loading = true;
|
|
51
|
-
error = '';
|
|
52
|
-
try {
|
|
53
|
-
const dict = await dc.assetDictionaries.getById(id, { asTree: true });
|
|
54
|
-
rootNodes = dict.nodes ?? [];
|
|
55
|
-
expanded = new Set(rootNodes.map(n => n.id));
|
|
56
|
-
} catch (err) {
|
|
57
|
-
error = err?.message ?? 'Failed to load the asset tree';
|
|
58
|
-
rootNodes = [];
|
|
59
|
-
} finally {
|
|
60
|
-
loading = false;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function onDictionaryChange(e) {
|
|
65
|
-
activeDictionaryId = e.target.value;
|
|
66
|
-
selectedNodeId = '';
|
|
67
|
-
if (client && activeDictionaryId) loadTree(client, activeDictionaryId);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function toggleExpand(id) {
|
|
71
|
-
const next = new Set(expanded);
|
|
72
|
-
next.has(id) ? next.delete(id) : next.add(id);
|
|
73
|
-
expanded = next;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function selectNode(node) {
|
|
77
|
-
selectedNodeId = node.id;
|
|
78
|
-
onSelectNode?.(collectEntryIds(node), node.id);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function retry() {
|
|
82
|
-
if (client) loadDictionaries(client);
|
|
83
|
-
}
|
|
84
|
-
</script>
|
|
85
|
-
|
|
86
|
-
<div class="asset-tree">
|
|
87
|
-
{#if dictionaries.length > 1}
|
|
88
|
-
<select class="dict-select" value={activeDictionaryId} onchange={onDictionaryChange}>
|
|
89
|
-
{#each dictionaries as dict (dict.id)}
|
|
90
|
-
<option value={dict.id}>{dict.name}</option>
|
|
91
|
-
{/each}
|
|
92
|
-
</select>
|
|
93
|
-
{/if}
|
|
94
|
-
|
|
95
|
-
{#if loading}
|
|
96
|
-
<div class="tree-state"><div class="spinner"></div> <span>Loading tree…</span></div>
|
|
97
|
-
{:else if error}
|
|
98
|
-
<div class="tree-state error">{error} — <button class="link-button" onclick={retry}>retry</button></div>
|
|
99
|
-
{:else if rootNodes.length === 0}
|
|
100
|
-
<div class="tree-state muted">No asset dictionary available.</div>
|
|
101
|
-
{:else}
|
|
102
|
-
<ul class="tree-root">
|
|
103
|
-
{#each rootNodes as node (node.id)}
|
|
104
|
-
{@render treeNode(node, 0)}
|
|
105
|
-
{/each}
|
|
106
|
-
</ul>
|
|
107
|
-
{/if}
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
{#snippet treeNode(node, depth)}
|
|
111
|
-
{@const hasChildren = node.children && node.children.length > 0}
|
|
112
|
-
{@const isOpen = expanded.has(node.id)}
|
|
113
|
-
<li class="tree-item">
|
|
114
|
-
<div
|
|
115
|
-
class="node-row"
|
|
116
|
-
class:selected={selectedNodeId === node.id}
|
|
117
|
-
style="padding-left: {depth * 1}rem"
|
|
118
|
-
>
|
|
119
|
-
{#if hasChildren}
|
|
120
|
-
<button type="button" class="twisty" class:open={isOpen} onclick={() => toggleExpand(node.id)} aria-label="Toggle">
|
|
121
|
-
<svg viewBox="0 0 16 16" width="12" height="12"><path d="M11 8L6 13V3z" fill="currentColor"></path></svg>
|
|
122
|
-
</button>
|
|
123
|
-
{:else}
|
|
124
|
-
<span class="twisty-spacer"></span>
|
|
125
|
-
{/if}
|
|
126
|
-
<button type="button" class="node-label" onclick={() => selectNode(node)}>
|
|
127
|
-
{node.name}
|
|
128
|
-
{#if node.entryIds && node.entryIds.length > 0}<span class="node-count">{node.entryIds.length}</span>{/if}
|
|
129
|
-
</button>
|
|
130
|
-
</div>
|
|
131
|
-
{#if hasChildren && isOpen}
|
|
132
|
-
<ul>
|
|
133
|
-
{#each node.children as child (child.id)}
|
|
134
|
-
{@render treeNode(child, depth + 1)}
|
|
135
|
-
{/each}
|
|
136
|
-
</ul>
|
|
137
|
-
{/if}
|
|
138
|
-
</li>
|
|
139
|
-
{/snippet}
|
|
140
|
-
|
|
141
|
-
<style>
|
|
142
|
-
.asset-tree { font-size: 0.8125rem; }
|
|
143
|
-
.dict-select {
|
|
144
|
-
width: 100%; margin-bottom: 0.5rem; padding: 0.375rem 0.5rem;
|
|
145
|
-
background: var(--cds-field-01, #353535); color: var(--cds-text-primary, #f4f4f4);
|
|
146
|
-
border: none; border-bottom: 1px solid var(--cds-border-strong-01, #6f6f6f); font-size: 0.8125rem;
|
|
147
|
-
}
|
|
148
|
-
.tree-state { display: flex; align-items: center; gap: 0.5rem; padding: 1rem; color: var(--cds-text-secondary, #c6c6c6); }
|
|
149
|
-
.tree-state.error { color: var(--cds-support-error, #fa4d56); }
|
|
150
|
-
.tree-state.muted { color: var(--cds-text-helper, #8d8d8d); }
|
|
151
|
-
.tree-root, .tree-item ul { list-style: none; margin: 0; padding: 0; }
|
|
152
|
-
.tree-root { max-height: 400px; overflow-y: auto; border: 1px solid var(--cds-border-subtle-01, #393939); }
|
|
153
|
-
.node-row { display: flex; align-items: center; gap: 0.25rem; cursor: pointer; }
|
|
154
|
-
.node-row:hover { background: var(--cds-layer-hover-01, #4c4c4c); }
|
|
155
|
-
.node-row.selected { background: var(--cds-layer-selected-01, #3d3d3d); }
|
|
156
|
-
.twisty, .twisty-spacer { width: 1.25rem; height: 1.5rem; flex-shrink: 0; }
|
|
157
|
-
.twisty { background: none; border: none; color: var(--cds-text-secondary, #c6c6c6); cursor: pointer; padding: 0; }
|
|
158
|
-
.twisty svg { transition: transform 0.15s ease; }
|
|
159
|
-
.twisty.open svg { transform: rotate(90deg); }
|
|
160
|
-
.node-label {
|
|
161
|
-
flex: 1; display: flex; align-items: center; gap: 0.375rem;
|
|
162
|
-
background: none; border: none; color: var(--cds-text-primary, #f4f4f4);
|
|
163
|
-
text-align: left; padding: 0.25rem 0.25rem; cursor: pointer; font-size: 0.8125rem;
|
|
164
|
-
}
|
|
165
|
-
.node-count {
|
|
166
|
-
font-size: 0.625rem; padding: 0 0.375rem; border-radius: 1rem;
|
|
167
|
-
background: var(--cds-tag-background-gray, #393939); color: var(--cds-tag-color-gray, #c6c6c6);
|
|
168
|
-
}
|
|
169
|
-
.spinner {
|
|
170
|
-
width: 1rem; height: 1rem;
|
|
171
|
-
border: 2px solid var(--cds-border-subtle-01, #e0e0e0);
|
|
172
|
-
border-top-color: var(--cds-interactive, #052FAD);
|
|
173
|
-
border-radius: 50%; animation: spin 0.8s linear infinite;
|
|
174
|
-
}
|
|
175
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
176
|
-
.link-button {
|
|
177
|
-
background: none; border: none; padding: 0; cursor: pointer;
|
|
178
|
-
color: var(--cds-link-primary, #78a9ff); text-decoration: underline; font-size: inherit;
|
|
179
|
-
}
|
|
180
|
-
</style>
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export default AssetTree;
|
|
2
|
-
type AssetTree = {
|
|
3
|
-
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
-
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
-
};
|
|
6
|
-
declare const AssetTree: import("svelte").Component<{
|
|
7
|
-
client?: any;
|
|
8
|
-
onSelectNode?: any;
|
|
9
|
-
}, {}, "">;
|
|
10
|
-
type $$ComponentProps = {
|
|
11
|
-
client?: any;
|
|
12
|
-
onSelectNode?: any;
|
|
13
|
-
};
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
/**
|
|
3
|
-
* LabelFacet — loads catalog labels and renders them as toggle chips.
|
|
4
|
-
* Selecting chips emits the active labelIds (OR filter) to the container,
|
|
5
|
-
* which owns the fetch. This component never fetches catalog entries.
|
|
6
|
-
*/
|
|
7
|
-
let {
|
|
8
|
-
client = null,
|
|
9
|
-
selectedLabelIds = $bindable([]),
|
|
10
|
-
onChange = null
|
|
11
|
-
} = $props();
|
|
12
|
-
|
|
13
|
-
let labels = $state([]);
|
|
14
|
-
let loading = $state(false);
|
|
15
|
-
let error = $state('');
|
|
16
|
-
|
|
17
|
-
/** Load labels once a client is available. */
|
|
18
|
-
$effect(() => {
|
|
19
|
-
if (client) loadLabels(client);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
async function loadLabels(dc) {
|
|
23
|
-
loading = true;
|
|
24
|
-
error = '';
|
|
25
|
-
try {
|
|
26
|
-
const result = await dc.labels.get();
|
|
27
|
-
labels = result.items ?? [];
|
|
28
|
-
} catch (err) {
|
|
29
|
-
error = err?.message ?? 'Failed to load labels';
|
|
30
|
-
labels = [];
|
|
31
|
-
} finally {
|
|
32
|
-
loading = false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function toggle(id) {
|
|
37
|
-
selectedLabelIds = selectedLabelIds.indexOf(id) >= 0
|
|
38
|
-
? selectedLabelIds.filter(x => x !== id)
|
|
39
|
-
: [...selectedLabelIds, id];
|
|
40
|
-
onChange?.(selectedLabelIds);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function clearAll() {
|
|
44
|
-
if (selectedLabelIds.length === 0) return;
|
|
45
|
-
selectedLabelIds = [];
|
|
46
|
-
onChange?.(selectedLabelIds);
|
|
47
|
-
}
|
|
48
|
-
</script>
|
|
49
|
-
|
|
50
|
-
{#if loading}
|
|
51
|
-
<div class="label-facet muted">Loading labels…</div>
|
|
52
|
-
{:else if error}
|
|
53
|
-
<div class="label-facet notice">Labels unavailable — search works without label filtering.</div>
|
|
54
|
-
{:else if labels.length > 0}
|
|
55
|
-
<div class="label-facet">
|
|
56
|
-
<span class="facet-label">Labels:</span>
|
|
57
|
-
{#each labels as label (label.id)}
|
|
58
|
-
{@const active = selectedLabelIds.includes(label.id)}
|
|
59
|
-
<button type="button" class="chip" class:active onclick={() => toggle(label.id)}>{label.name}</button>
|
|
60
|
-
{/each}
|
|
61
|
-
{#if selectedLabelIds.length > 0}
|
|
62
|
-
<button type="button" class="chip clear" onclick={clearAll}>Clear</button>
|
|
63
|
-
{/if}
|
|
64
|
-
</div>
|
|
65
|
-
{/if}
|
|
66
|
-
|
|
67
|
-
<style>
|
|
68
|
-
.label-facet {
|
|
69
|
-
display: flex; flex-wrap: wrap; align-items: center; gap: 0.375rem;
|
|
70
|
-
padding: 0.5rem 0; font-size: 0.75rem;
|
|
71
|
-
}
|
|
72
|
-
.facet-label { color: var(--cds-text-secondary, #c6c6c6); margin-right: 0.25rem; }
|
|
73
|
-
.muted { color: var(--cds-text-helper, #8d8d8d); }
|
|
74
|
-
.notice { color: var(--cds-support-warning, #f1c21b); }
|
|
75
|
-
.chip {
|
|
76
|
-
padding: 0.125rem 0.5rem;
|
|
77
|
-
background: var(--cds-tag-background-gray, #393939);
|
|
78
|
-
color: var(--cds-tag-color-gray, #c6c6c6);
|
|
79
|
-
border: 1px solid transparent; border-radius: 1rem;
|
|
80
|
-
font-size: 0.6875rem; cursor: pointer; transition: background-color 0.15s, border-color 0.15s;
|
|
81
|
-
}
|
|
82
|
-
.chip:hover { background: var(--cds-layer-hover-01, #4c4c4c); }
|
|
83
|
-
.chip.active {
|
|
84
|
-
background: var(--cds-layer-selected-01, #052FAD);
|
|
85
|
-
color: var(--cds-text-on-color, #ffffff);
|
|
86
|
-
border-color: var(--cds-focus, #052FAD);
|
|
87
|
-
}
|
|
88
|
-
.chip.clear { background: transparent; border-color: var(--cds-border-strong-01, #6f6f6f); }
|
|
89
|
-
</style>
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export default LabelFacet;
|
|
2
|
-
type LabelFacet = {
|
|
3
|
-
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
-
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
-
};
|
|
6
|
-
declare const LabelFacet: import("svelte").Component<{
|
|
7
|
-
client?: any;
|
|
8
|
-
selectedLabelIds?: any[];
|
|
9
|
-
onChange?: any;
|
|
10
|
-
}, {}, "selectedLabelIds">;
|
|
11
|
-
type $$ComponentProps = {
|
|
12
|
-
client?: any;
|
|
13
|
-
selectedLabelIds?: any[];
|
|
14
|
-
onChange?: any;
|
|
15
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { AssetNode, AssetDictionary } from '@industream/datacatalog-client/dto';
|
|
2
|
-
import type { CatalogEntry } from '@industream/datacatalog-client/dto';
|
|
3
|
-
/** Depth-first flatten of a node forest — self emitted before its children. */
|
|
4
|
-
export declare function flattenNodes(nodes: AssetNode[]): AssetNode[];
|
|
5
|
-
/** Find a node by id anywhere in the forest; null when absent. */
|
|
6
|
-
export declare function findNodeById(nodes: AssetNode[], id: string): AssetNode | null;
|
|
7
|
-
/**
|
|
8
|
-
* Collect a node's assigned parent entryIds. With `includeDescendants`, unions
|
|
9
|
-
* every descendant's entryIds too. De-duplicated, node-first, DFS order.
|
|
10
|
-
*/
|
|
11
|
-
export declare function collectEntryIds(node: AssetNode, includeDescendants?: boolean): string[];
|
|
12
|
-
/**
|
|
13
|
-
* Keep only binding rows on the given source connection. An empty
|
|
14
|
-
* `sourceConnectionId` disables filtering (returns the input unchanged).
|
|
15
|
-
* The typed `query` filter cannot scope by connection, so the container
|
|
16
|
-
* filters client-side (node entry counts are small).
|
|
17
|
-
*/
|
|
18
|
-
export declare function filterEntriesByConnection(entries: CatalogEntry[], sourceConnectionId: string): CatalogEntry[];
|
|
19
|
-
/** Id of the first dictionary (default selection), or undefined for an empty list. */
|
|
20
|
-
export declare function firstDictionaryId(dictionaries: AssetDictionary[]): string | undefined;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/** Depth-first flatten of a node forest — self emitted before its children. */
|
|
2
|
-
export function flattenNodes(nodes) {
|
|
3
|
-
const result = [];
|
|
4
|
-
for (const n of nodes) {
|
|
5
|
-
result.push(n);
|
|
6
|
-
if (n.children && n.children.length > 0) {
|
|
7
|
-
result.push(...flattenNodes(n.children));
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
return result;
|
|
11
|
-
}
|
|
12
|
-
/** Find a node by id anywhere in the forest; null when absent. */
|
|
13
|
-
export function findNodeById(nodes, id) {
|
|
14
|
-
for (const n of flattenNodes(nodes)) {
|
|
15
|
-
if (n.id === id)
|
|
16
|
-
return n;
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Collect a node's assigned parent entryIds. With `includeDescendants`, unions
|
|
22
|
-
* every descendant's entryIds too. De-duplicated, node-first, DFS order.
|
|
23
|
-
*/
|
|
24
|
-
export function collectEntryIds(node, includeDescendants = false) {
|
|
25
|
-
const source = includeDescendants ? flattenNodes([node]) : [node];
|
|
26
|
-
const seen = new Set();
|
|
27
|
-
const result = [];
|
|
28
|
-
for (const n of source) {
|
|
29
|
-
for (const id of n.entryIds ?? []) {
|
|
30
|
-
if (!seen.has(id)) {
|
|
31
|
-
seen.add(id);
|
|
32
|
-
result.push(id);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Keep only binding rows on the given source connection. An empty
|
|
40
|
-
* `sourceConnectionId` disables filtering (returns the input unchanged).
|
|
41
|
-
* The typed `query` filter cannot scope by connection, so the container
|
|
42
|
-
* filters client-side (node entry counts are small).
|
|
43
|
-
*/
|
|
44
|
-
export function filterEntriesByConnection(entries, sourceConnectionId) {
|
|
45
|
-
if (!sourceConnectionId)
|
|
46
|
-
return entries;
|
|
47
|
-
return entries.filter(e => e.sourceConnection?.id === sourceConnectionId);
|
|
48
|
-
}
|
|
49
|
-
/** Id of the first dictionary (default selection), or undefined for an empty list. */
|
|
50
|
-
export function firstDictionaryId(dictionaries) {
|
|
51
|
-
return dictionaries.length > 0 ? dictionaries[0].id : undefined;
|
|
52
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { flattenNodes, findNodeById, collectEntryIds, filterEntriesByConnection, firstDictionaryId } from './picker-tree';
|
|
3
|
-
const node = (id, entryIds = [], children = []) => ({ id, dictionaryId: 'd1', name: id, order: 0, entryIds, children });
|
|
4
|
-
const tree = [
|
|
5
|
-
node('root', ['e1'], [
|
|
6
|
-
node('zone-a', ['e2', 'e3'], [
|
|
7
|
-
node('leaf', ['e3', 'e4'])
|
|
8
|
-
]),
|
|
9
|
-
node('zone-b', ['e5'])
|
|
10
|
-
])
|
|
11
|
-
];
|
|
12
|
-
describe('flattenNodes', () => {
|
|
13
|
-
it('returns every node depth-first, self before children', () => {
|
|
14
|
-
expect(flattenNodes(tree).map(n => n.id)).toEqual(['root', 'zone-a', 'leaf', 'zone-b']);
|
|
15
|
-
});
|
|
16
|
-
it('handles an empty forest', () => {
|
|
17
|
-
expect(flattenNodes([])).toEqual([]);
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
describe('findNodeById', () => {
|
|
21
|
-
it('finds a nested node', () => {
|
|
22
|
-
expect(findNodeById(tree, 'leaf')?.id).toBe('leaf');
|
|
23
|
-
});
|
|
24
|
-
it('returns null when absent', () => {
|
|
25
|
-
expect(findNodeById(tree, 'nope')).toBeNull();
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
describe('collectEntryIds', () => {
|
|
29
|
-
it('returns only the node own entryIds by default', () => {
|
|
30
|
-
expect(collectEntryIds(findNodeById(tree, 'zone-a'))).toEqual(['e2', 'e3']);
|
|
31
|
-
});
|
|
32
|
-
it('unions descendants and de-duplicates, node first, DFS order', () => {
|
|
33
|
-
expect(collectEntryIds(findNodeById(tree, 'zone-a'), true)).toEqual(['e2', 'e3', 'e4']);
|
|
34
|
-
});
|
|
35
|
-
it('collects the whole tree from the root with descendants', () => {
|
|
36
|
-
expect(collectEntryIds(tree[0], true)).toEqual(['e1', 'e2', 'e3', 'e4', 'e5']);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
describe('filterEntriesByConnection', () => {
|
|
40
|
-
const entry = (id, connId) => ({ id, sourceConnection: { id: connId } });
|
|
41
|
-
const rows = [entry('a', 'c1'), entry('b', 'c2'), entry('c', 'c1')];
|
|
42
|
-
it('keeps only rows on the given connection', () => {
|
|
43
|
-
expect(filterEntriesByConnection(rows, 'c1').map(e => e.id)).toEqual(['a', 'c']);
|
|
44
|
-
});
|
|
45
|
-
it('returns all rows when connection id is empty', () => {
|
|
46
|
-
expect(filterEntriesByConnection(rows, '')).toHaveLength(3);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
describe('firstDictionaryId', () => {
|
|
50
|
-
it('returns the first id or undefined', () => {
|
|
51
|
-
expect(firstDictionaryId([{ id: 'x' }])).toBe('x');
|
|
52
|
-
expect(firstDictionaryId([])).toBeUndefined();
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
/**
|
|
3
|
-
* AssetTree — loads an asset dictionary as a tree and lets the user pick a node.
|
|
4
|
-
* Emits the selected node's own entryIds to the container, which fetches and
|
|
5
|
-
* filters the entries. This component holds no selection or entry state.
|
|
6
|
-
*/
|
|
7
|
-
import { firstDictionaryId, collectEntryIds } from './picker-tree';
|
|
8
|
-
|
|
9
|
-
let {
|
|
10
|
-
client = null,
|
|
11
|
-
onSelectNode = null
|
|
12
|
-
} = $props();
|
|
13
|
-
|
|
14
|
-
let dictionaries = $state([]);
|
|
15
|
-
let activeDictionaryId = $state('');
|
|
16
|
-
let rootNodes = $state([]);
|
|
17
|
-
let expanded = $state(new Set());
|
|
18
|
-
let selectedNodeId = $state('');
|
|
19
|
-
let loading = $state(false);
|
|
20
|
-
let error = $state('');
|
|
21
|
-
|
|
22
|
-
/** Load the dictionary list when a client becomes available. */
|
|
23
|
-
$effect(() => {
|
|
24
|
-
if (client) loadDictionaries(client);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
async function loadDictionaries(dc) {
|
|
28
|
-
loading = true;
|
|
29
|
-
error = '';
|
|
30
|
-
try {
|
|
31
|
-
const result = await dc.assetDictionaries.get({ asTree: true });
|
|
32
|
-
dictionaries = result.items ?? [];
|
|
33
|
-
const first = firstDictionaryId(dictionaries);
|
|
34
|
-
if (first) {
|
|
35
|
-
activeDictionaryId = first;
|
|
36
|
-
await loadTree(dc, first);
|
|
37
|
-
} else {
|
|
38
|
-
rootNodes = [];
|
|
39
|
-
}
|
|
40
|
-
} catch (err) {
|
|
41
|
-
error = err?.message ?? 'Failed to load asset dictionaries';
|
|
42
|
-
dictionaries = [];
|
|
43
|
-
rootNodes = [];
|
|
44
|
-
} finally {
|
|
45
|
-
loading = false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function loadTree(dc, id) {
|
|
50
|
-
loading = true;
|
|
51
|
-
error = '';
|
|
52
|
-
try {
|
|
53
|
-
const dict = await dc.assetDictionaries.getById(id, { asTree: true });
|
|
54
|
-
rootNodes = dict.nodes ?? [];
|
|
55
|
-
expanded = new Set(rootNodes.map(n => n.id));
|
|
56
|
-
} catch (err) {
|
|
57
|
-
error = err?.message ?? 'Failed to load the asset tree';
|
|
58
|
-
rootNodes = [];
|
|
59
|
-
} finally {
|
|
60
|
-
loading = false;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function onDictionaryChange(e) {
|
|
65
|
-
activeDictionaryId = e.target.value;
|
|
66
|
-
selectedNodeId = '';
|
|
67
|
-
if (client && activeDictionaryId) loadTree(client, activeDictionaryId);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function toggleExpand(id) {
|
|
71
|
-
const next = new Set(expanded);
|
|
72
|
-
next.has(id) ? next.delete(id) : next.add(id);
|
|
73
|
-
expanded = next;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function selectNode(node) {
|
|
77
|
-
selectedNodeId = node.id;
|
|
78
|
-
onSelectNode?.(collectEntryIds(node), node.id);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function retry() {
|
|
82
|
-
if (client) loadDictionaries(client);
|
|
83
|
-
}
|
|
84
|
-
</script>
|
|
85
|
-
|
|
86
|
-
<div class="asset-tree">
|
|
87
|
-
{#if dictionaries.length > 1}
|
|
88
|
-
<select class="dict-select" value={activeDictionaryId} onchange={onDictionaryChange}>
|
|
89
|
-
{#each dictionaries as dict (dict.id)}
|
|
90
|
-
<option value={dict.id}>{dict.name}</option>
|
|
91
|
-
{/each}
|
|
92
|
-
</select>
|
|
93
|
-
{/if}
|
|
94
|
-
|
|
95
|
-
{#if loading}
|
|
96
|
-
<div class="tree-state"><div class="spinner"></div> <span>Loading tree…</span></div>
|
|
97
|
-
{:else if error}
|
|
98
|
-
<div class="tree-state error">{error} — <button class="link-button" onclick={retry}>retry</button></div>
|
|
99
|
-
{:else if rootNodes.length === 0}
|
|
100
|
-
<div class="tree-state muted">No asset dictionary available.</div>
|
|
101
|
-
{:else}
|
|
102
|
-
<ul class="tree-root">
|
|
103
|
-
{#each rootNodes as node (node.id)}
|
|
104
|
-
{@render treeNode(node, 0)}
|
|
105
|
-
{/each}
|
|
106
|
-
</ul>
|
|
107
|
-
{/if}
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
{#snippet treeNode(node, depth)}
|
|
111
|
-
{@const hasChildren = node.children && node.children.length > 0}
|
|
112
|
-
{@const isOpen = expanded.has(node.id)}
|
|
113
|
-
<li class="tree-item">
|
|
114
|
-
<div
|
|
115
|
-
class="node-row"
|
|
116
|
-
class:selected={selectedNodeId === node.id}
|
|
117
|
-
style="padding-left: {depth * 1}rem"
|
|
118
|
-
>
|
|
119
|
-
{#if hasChildren}
|
|
120
|
-
<button type="button" class="twisty" class:open={isOpen} onclick={() => toggleExpand(node.id)} aria-label="Toggle">
|
|
121
|
-
<svg viewBox="0 0 16 16" width="12" height="12"><path d="M11 8L6 13V3z" fill="currentColor"></path></svg>
|
|
122
|
-
</button>
|
|
123
|
-
{:else}
|
|
124
|
-
<span class="twisty-spacer"></span>
|
|
125
|
-
{/if}
|
|
126
|
-
<button type="button" class="node-label" onclick={() => selectNode(node)}>
|
|
127
|
-
{node.name}
|
|
128
|
-
{#if node.entryIds && node.entryIds.length > 0}<span class="node-count">{node.entryIds.length}</span>{/if}
|
|
129
|
-
</button>
|
|
130
|
-
</div>
|
|
131
|
-
{#if hasChildren && isOpen}
|
|
132
|
-
<ul>
|
|
133
|
-
{#each node.children as child (child.id)}
|
|
134
|
-
{@render treeNode(child, depth + 1)}
|
|
135
|
-
{/each}
|
|
136
|
-
</ul>
|
|
137
|
-
{/if}
|
|
138
|
-
</li>
|
|
139
|
-
{/snippet}
|
|
140
|
-
|
|
141
|
-
<style>
|
|
142
|
-
.asset-tree { font-size: 0.8125rem; }
|
|
143
|
-
.dict-select {
|
|
144
|
-
width: 100%; margin-bottom: 0.5rem; padding: 0.375rem 0.5rem;
|
|
145
|
-
background: var(--cds-field-01, #353535); color: var(--cds-text-primary, #f4f4f4);
|
|
146
|
-
border: none; border-bottom: 1px solid var(--cds-border-strong-01, #6f6f6f); font-size: 0.8125rem;
|
|
147
|
-
}
|
|
148
|
-
.tree-state { display: flex; align-items: center; gap: 0.5rem; padding: 1rem; color: var(--cds-text-secondary, #c6c6c6); }
|
|
149
|
-
.tree-state.error { color: var(--cds-support-error, #fa4d56); }
|
|
150
|
-
.tree-state.muted { color: var(--cds-text-helper, #8d8d8d); }
|
|
151
|
-
.tree-root, .tree-item ul { list-style: none; margin: 0; padding: 0; }
|
|
152
|
-
.tree-root { max-height: 400px; overflow-y: auto; border: 1px solid var(--cds-border-subtle-01, #393939); }
|
|
153
|
-
.node-row { display: flex; align-items: center; gap: 0.25rem; cursor: pointer; }
|
|
154
|
-
.node-row:hover { background: var(--cds-layer-hover-01, #4c4c4c); }
|
|
155
|
-
.node-row.selected { background: var(--cds-layer-selected-01, #3d3d3d); }
|
|
156
|
-
.twisty, .twisty-spacer { width: 1.25rem; height: 1.5rem; flex-shrink: 0; }
|
|
157
|
-
.twisty { background: none; border: none; color: var(--cds-text-secondary, #c6c6c6); cursor: pointer; padding: 0; }
|
|
158
|
-
.twisty svg { transition: transform 0.15s ease; }
|
|
159
|
-
.twisty.open svg { transform: rotate(90deg); }
|
|
160
|
-
.node-label {
|
|
161
|
-
flex: 1; display: flex; align-items: center; gap: 0.375rem;
|
|
162
|
-
background: none; border: none; color: var(--cds-text-primary, #f4f4f4);
|
|
163
|
-
text-align: left; padding: 0.25rem 0.25rem; cursor: pointer; font-size: 0.8125rem;
|
|
164
|
-
}
|
|
165
|
-
.node-count {
|
|
166
|
-
font-size: 0.625rem; padding: 0 0.375rem; border-radius: 1rem;
|
|
167
|
-
background: var(--cds-tag-background-gray, #393939); color: var(--cds-tag-color-gray, #c6c6c6);
|
|
168
|
-
}
|
|
169
|
-
.spinner {
|
|
170
|
-
width: 1rem; height: 1rem;
|
|
171
|
-
border: 2px solid var(--cds-border-subtle-01, #e0e0e0);
|
|
172
|
-
border-top-color: var(--cds-interactive, #052FAD);
|
|
173
|
-
border-radius: 50%; animation: spin 0.8s linear infinite;
|
|
174
|
-
}
|
|
175
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
176
|
-
.link-button {
|
|
177
|
-
background: none; border: none; padding: 0; cursor: pointer;
|
|
178
|
-
color: var(--cds-link-primary, #78a9ff); text-decoration: underline; font-size: inherit;
|
|
179
|
-
}
|
|
180
|
-
</style>
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
/**
|
|
3
|
-
* LabelFacet — loads catalog labels and renders them as toggle chips.
|
|
4
|
-
* Selecting chips emits the active labelIds (OR filter) to the container,
|
|
5
|
-
* which owns the fetch. This component never fetches catalog entries.
|
|
6
|
-
*/
|
|
7
|
-
let {
|
|
8
|
-
client = null,
|
|
9
|
-
selectedLabelIds = $bindable([]),
|
|
10
|
-
onChange = null
|
|
11
|
-
} = $props();
|
|
12
|
-
|
|
13
|
-
let labels = $state([]);
|
|
14
|
-
let loading = $state(false);
|
|
15
|
-
let error = $state('');
|
|
16
|
-
|
|
17
|
-
/** Load labels once a client is available. */
|
|
18
|
-
$effect(() => {
|
|
19
|
-
if (client) loadLabels(client);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
async function loadLabels(dc) {
|
|
23
|
-
loading = true;
|
|
24
|
-
error = '';
|
|
25
|
-
try {
|
|
26
|
-
const result = await dc.labels.get();
|
|
27
|
-
labels = result.items ?? [];
|
|
28
|
-
} catch (err) {
|
|
29
|
-
error = err?.message ?? 'Failed to load labels';
|
|
30
|
-
labels = [];
|
|
31
|
-
} finally {
|
|
32
|
-
loading = false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function toggle(id) {
|
|
37
|
-
selectedLabelIds = selectedLabelIds.indexOf(id) >= 0
|
|
38
|
-
? selectedLabelIds.filter(x => x !== id)
|
|
39
|
-
: [...selectedLabelIds, id];
|
|
40
|
-
onChange?.(selectedLabelIds);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function clearAll() {
|
|
44
|
-
if (selectedLabelIds.length === 0) return;
|
|
45
|
-
selectedLabelIds = [];
|
|
46
|
-
onChange?.(selectedLabelIds);
|
|
47
|
-
}
|
|
48
|
-
</script>
|
|
49
|
-
|
|
50
|
-
{#if loading}
|
|
51
|
-
<div class="label-facet muted">Loading labels…</div>
|
|
52
|
-
{:else if error}
|
|
53
|
-
<div class="label-facet notice">Labels unavailable — search works without label filtering.</div>
|
|
54
|
-
{:else if labels.length > 0}
|
|
55
|
-
<div class="label-facet">
|
|
56
|
-
<span class="facet-label">Labels:</span>
|
|
57
|
-
{#each labels as label (label.id)}
|
|
58
|
-
{@const active = selectedLabelIds.includes(label.id)}
|
|
59
|
-
<button type="button" class="chip" class:active onclick={() => toggle(label.id)}>{label.name}</button>
|
|
60
|
-
{/each}
|
|
61
|
-
{#if selectedLabelIds.length > 0}
|
|
62
|
-
<button type="button" class="chip clear" onclick={clearAll}>Clear</button>
|
|
63
|
-
{/if}
|
|
64
|
-
</div>
|
|
65
|
-
{/if}
|
|
66
|
-
|
|
67
|
-
<style>
|
|
68
|
-
.label-facet {
|
|
69
|
-
display: flex; flex-wrap: wrap; align-items: center; gap: 0.375rem;
|
|
70
|
-
padding: 0.5rem 0; font-size: 0.75rem;
|
|
71
|
-
}
|
|
72
|
-
.facet-label { color: var(--cds-text-secondary, #c6c6c6); margin-right: 0.25rem; }
|
|
73
|
-
.muted { color: var(--cds-text-helper, #8d8d8d); }
|
|
74
|
-
.notice { color: var(--cds-support-warning, #f1c21b); }
|
|
75
|
-
.chip {
|
|
76
|
-
padding: 0.125rem 0.5rem;
|
|
77
|
-
background: var(--cds-tag-background-gray, #393939);
|
|
78
|
-
color: var(--cds-tag-color-gray, #c6c6c6);
|
|
79
|
-
border: 1px solid transparent; border-radius: 1rem;
|
|
80
|
-
font-size: 0.6875rem; cursor: pointer; transition: background-color 0.15s, border-color 0.15s;
|
|
81
|
-
}
|
|
82
|
-
.chip:hover { background: var(--cds-layer-hover-01, #4c4c4c); }
|
|
83
|
-
.chip.active {
|
|
84
|
-
background: var(--cds-layer-selected-01, #052FAD);
|
|
85
|
-
color: var(--cds-text-on-color, #ffffff);
|
|
86
|
-
border-color: var(--cds-focus, #052FAD);
|
|
87
|
-
}
|
|
88
|
-
.chip.clear { background: transparent; border-color: var(--cds-border-strong-01, #6f6f6f); }
|
|
89
|
-
</style>
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import type { AssetNode, AssetDictionary, CatalogEntry } from '@industream/datacatalog-client/dto';
|
|
3
|
-
import {
|
|
4
|
-
flattenNodes,
|
|
5
|
-
findNodeById,
|
|
6
|
-
collectEntryIds,
|
|
7
|
-
filterEntriesByConnection,
|
|
8
|
-
firstDictionaryId
|
|
9
|
-
} from './picker-tree';
|
|
10
|
-
|
|
11
|
-
const node = (id: string, entryIds: string[] = [], children: AssetNode[] = []): AssetNode =>
|
|
12
|
-
({ id, dictionaryId: 'd1', name: id, order: 0, entryIds, children } as AssetNode);
|
|
13
|
-
|
|
14
|
-
const tree: AssetNode[] = [
|
|
15
|
-
node('root', ['e1'], [
|
|
16
|
-
node('zone-a', ['e2', 'e3'], [
|
|
17
|
-
node('leaf', ['e3', 'e4'])
|
|
18
|
-
]),
|
|
19
|
-
node('zone-b', ['e5'])
|
|
20
|
-
])
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
describe('flattenNodes', () => {
|
|
24
|
-
it('returns every node depth-first, self before children', () => {
|
|
25
|
-
expect(flattenNodes(tree).map(n => n.id)).toEqual(['root', 'zone-a', 'leaf', 'zone-b']);
|
|
26
|
-
});
|
|
27
|
-
it('handles an empty forest', () => {
|
|
28
|
-
expect(flattenNodes([])).toEqual([]);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('findNodeById', () => {
|
|
33
|
-
it('finds a nested node', () => {
|
|
34
|
-
expect(findNodeById(tree, 'leaf')?.id).toBe('leaf');
|
|
35
|
-
});
|
|
36
|
-
it('returns null when absent', () => {
|
|
37
|
-
expect(findNodeById(tree, 'nope')).toBeNull();
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe('collectEntryIds', () => {
|
|
42
|
-
it('returns only the node own entryIds by default', () => {
|
|
43
|
-
expect(collectEntryIds(findNodeById(tree, 'zone-a')!)).toEqual(['e2', 'e3']);
|
|
44
|
-
});
|
|
45
|
-
it('unions descendants and de-duplicates, node first, DFS order', () => {
|
|
46
|
-
expect(collectEntryIds(findNodeById(tree, 'zone-a')!, true)).toEqual(['e2', 'e3', 'e4']);
|
|
47
|
-
});
|
|
48
|
-
it('collects the whole tree from the root with descendants', () => {
|
|
49
|
-
expect(collectEntryIds(tree[0], true)).toEqual(['e1', 'e2', 'e3', 'e4', 'e5']);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('filterEntriesByConnection', () => {
|
|
54
|
-
const entry = (id: string, connId: string): CatalogEntry =>
|
|
55
|
-
({ id, sourceConnection: { id: connId } } as CatalogEntry);
|
|
56
|
-
const rows = [entry('a', 'c1'), entry('b', 'c2'), entry('c', 'c1')];
|
|
57
|
-
it('keeps only rows on the given connection', () => {
|
|
58
|
-
expect(filterEntriesByConnection(rows, 'c1').map(e => e.id)).toEqual(['a', 'c']);
|
|
59
|
-
});
|
|
60
|
-
it('returns all rows when connection id is empty', () => {
|
|
61
|
-
expect(filterEntriesByConnection(rows, '')).toHaveLength(3);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe('firstDictionaryId', () => {
|
|
66
|
-
it('returns the first id or undefined', () => {
|
|
67
|
-
expect(firstDictionaryId([{ id: 'x' } as AssetDictionary])).toBe('x');
|
|
68
|
-
expect(firstDictionaryId([])).toBeUndefined();
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { AssetNode, AssetDictionary } from '@industream/datacatalog-client/dto';
|
|
2
|
-
import type { CatalogEntry } from '@industream/datacatalog-client/dto';
|
|
3
|
-
|
|
4
|
-
/** Depth-first flatten of a node forest — self emitted before its children. */
|
|
5
|
-
export function flattenNodes(nodes: AssetNode[]): AssetNode[] {
|
|
6
|
-
const result: AssetNode[] = [];
|
|
7
|
-
for (const n of nodes) {
|
|
8
|
-
result.push(n);
|
|
9
|
-
if (n.children && n.children.length > 0) {
|
|
10
|
-
result.push(...flattenNodes(n.children));
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
return result;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Find a node by id anywhere in the forest; null when absent. */
|
|
17
|
-
export function findNodeById(nodes: AssetNode[], id: string): AssetNode | null {
|
|
18
|
-
for (const n of flattenNodes(nodes)) {
|
|
19
|
-
if (n.id === id) return n;
|
|
20
|
-
}
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Collect a node's assigned parent entryIds. With `includeDescendants`, unions
|
|
26
|
-
* every descendant's entryIds too. De-duplicated, node-first, DFS order.
|
|
27
|
-
*/
|
|
28
|
-
export function collectEntryIds(node: AssetNode, includeDescendants: boolean = false): string[] {
|
|
29
|
-
const source = includeDescendants ? flattenNodes([node]) : [node];
|
|
30
|
-
const seen = new Set<string>();
|
|
31
|
-
const result: string[] = [];
|
|
32
|
-
for (const n of source) {
|
|
33
|
-
for (const id of n.entryIds ?? []) {
|
|
34
|
-
if (!seen.has(id)) {
|
|
35
|
-
seen.add(id);
|
|
36
|
-
result.push(id);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return result;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Keep only binding rows on the given source connection. An empty
|
|
45
|
-
* `sourceConnectionId` disables filtering (returns the input unchanged).
|
|
46
|
-
* The typed `query` filter cannot scope by connection, so the container
|
|
47
|
-
* filters client-side (node entry counts are small).
|
|
48
|
-
*/
|
|
49
|
-
export function filterEntriesByConnection(
|
|
50
|
-
entries: CatalogEntry[],
|
|
51
|
-
sourceConnectionId: string
|
|
52
|
-
): CatalogEntry[] {
|
|
53
|
-
if (!sourceConnectionId) return entries;
|
|
54
|
-
return entries.filter(e => e.sourceConnection?.id === sourceConnectionId);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Id of the first dictionary (default selection), or undefined for an empty list. */
|
|
58
|
-
export function firstDictionaryId(dictionaries: AssetDictionary[]): string | undefined {
|
|
59
|
-
return dictionaries.length > 0 ? dictionaries[0].id : undefined;
|
|
60
|
-
}
|