@industream/flowmaker-flowbox-ui-components 0.0.12 → 0.0.13
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/dist/DCCatalogEntry.svelte +22 -133
- package/dist/DCCatalogEntry.svelte.d.ts +3 -10
- package/dist/DCCatalogEntryPicker/DCCatalogEntryPicker.svelte +113 -0
- package/dist/DCCatalogEntryPicker/DCCatalogEntryPicker.svelte.d.ts +15 -0
- package/dist/DCCatalogEntryPicker/SelectedTags.svelte +150 -0
- package/dist/DCCatalogEntryPicker/SelectedTags.svelte.d.ts +18 -0
- package/dist/DCCatalogEntryPicker/TagBrowser.svelte +210 -0
- package/dist/DCCatalogEntryPicker/TagBrowser.svelte.d.ts +24 -0
- package/dist/DCCatalogEntryPicker/picker-column-helpers.d.ts +74 -0
- package/dist/DCCatalogEntryPicker/picker-column-helpers.js +105 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/package.json +10 -1
- package/src/DCCatalogEntry.svelte +22 -133
- package/src/DCCatalogEntryPicker/DCCatalogEntryPicker.svelte +113 -0
- package/src/DCCatalogEntryPicker/SelectedTags.svelte +150 -0
- package/src/DCCatalogEntryPicker/TagBrowser.svelte +210 -0
- package/src/DCCatalogEntryPicker/picker-column-helpers.ts +126 -0
- package/src/index.ts +3 -1
|
@@ -2,11 +2,6 @@
|
|
|
2
2
|
import { DataCatalogClient } from '@industream/datacatalog-client';
|
|
3
3
|
import type { CatalogEntry, DataType, SourceType } from '@industream/datacatalog-client/dto';
|
|
4
4
|
|
|
5
|
-
interface FilterOptions {
|
|
6
|
-
searchtext?: string | null; // Case-insensitive LIKE %text% filter on entry name
|
|
7
|
-
datasetfilter?: string | null; // Filter entries by sourceParams.dataset
|
|
8
|
-
}
|
|
9
|
-
|
|
10
5
|
interface Props {
|
|
11
6
|
id?: string;
|
|
12
7
|
dcapiurl?: string;
|
|
@@ -14,9 +9,8 @@
|
|
|
14
9
|
datatypefilter?: DataType | DataType[] | null;
|
|
15
10
|
namefilter?: string | string[] | null;
|
|
16
11
|
initialselection?: string | null;
|
|
17
|
-
onentryselect?: (entry: CatalogEntry
|
|
18
|
-
onitemsloaded?: (
|
|
19
|
-
onsearchmiss?: (text: string) => void; // Called when searchtext matches nothing
|
|
12
|
+
onentryselect?: (entry: CatalogEntry) => void;
|
|
13
|
+
onitemsloaded?: (entries: CatalogEntry[]) => void;
|
|
20
14
|
}
|
|
21
15
|
|
|
22
16
|
let {
|
|
@@ -27,8 +21,7 @@
|
|
|
27
21
|
namefilter = null,
|
|
28
22
|
initialselection = null,
|
|
29
23
|
onentryselect = null,
|
|
30
|
-
onitemsloaded = null
|
|
31
|
-
onsearchmiss = null
|
|
24
|
+
onitemsloaded = null
|
|
32
25
|
}: Props = $props();
|
|
33
26
|
|
|
34
27
|
let catalogEntries = $state<CatalogEntry[]>([]);
|
|
@@ -38,13 +31,6 @@
|
|
|
38
31
|
let error = $state<string | null>(null);
|
|
39
32
|
let dropdownRef = $state<HTMLElement | null>(null);
|
|
40
33
|
|
|
41
|
-
// Active dynamic filters (set via setFilters method)
|
|
42
|
-
let activeSearchtext: string | null = null;
|
|
43
|
-
let activeDatasetfilter: string | null = null;
|
|
44
|
-
let autoSelectedId: string | null = null;
|
|
45
|
-
let searchMissText = $state<string | null>(null);
|
|
46
|
-
let multipleMatchText = $state<string | null>(null);
|
|
47
|
-
|
|
48
34
|
// Load catalog entries when component mounts or dcapiurl/sourcetypefilter changes
|
|
49
35
|
$effect(() => {
|
|
50
36
|
// Read sourcetypefilter to establish dependency tracking
|
|
@@ -54,6 +40,11 @@
|
|
|
54
40
|
}
|
|
55
41
|
});
|
|
56
42
|
|
|
43
|
+
// Apply client-side filters when entries or filters change
|
|
44
|
+
$effect(() => {
|
|
45
|
+
filteredEntries = applyFilters(catalogEntries);
|
|
46
|
+
});
|
|
47
|
+
|
|
57
48
|
// Inject styles into shadow DOM to fix trigger-label width
|
|
58
49
|
$effect(() => {
|
|
59
50
|
if (dropdownRef) {
|
|
@@ -69,10 +60,11 @@
|
|
|
69
60
|
}
|
|
70
61
|
});
|
|
71
62
|
|
|
72
|
-
function
|
|
63
|
+
function applyFilters(entries: CatalogEntry[]): CatalogEntry[] {
|
|
73
64
|
let result = entries;
|
|
74
65
|
|
|
75
66
|
// Filter by source type name (from sourceConnection.sourceType.name)
|
|
67
|
+
// Note: Server-side filtering is now done via API, this is just for additional client-side filtering if needed
|
|
76
68
|
if (sourcetypefilter && sourcetypefilter.length > 0) {
|
|
77
69
|
const sourceTypeNames = Array.isArray(sourcetypefilter) ? sourcetypefilter : [sourcetypefilter];
|
|
78
70
|
result = result.filter(entry =>
|
|
@@ -92,54 +84,6 @@
|
|
|
92
84
|
return result;
|
|
93
85
|
}
|
|
94
86
|
|
|
95
|
-
function applyDynamicFilters(entries: CatalogEntry[], { skipSearch = false } = {}): CatalogEntry[] {
|
|
96
|
-
let result = entries;
|
|
97
|
-
|
|
98
|
-
if (activeDatasetfilter) {
|
|
99
|
-
result = result.filter(entry =>
|
|
100
|
-
entry.sourceParams?.dataset === activeDatasetfilter
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!skipSearch && activeSearchtext?.trim()) {
|
|
105
|
-
const needle = activeSearchtext.trim().toLowerCase();
|
|
106
|
-
result = result.filter(entry =>
|
|
107
|
-
entry.name?.toLowerCase().includes(needle)
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function runFilters() {
|
|
115
|
-
const base = applyBaseFilters(catalogEntries);
|
|
116
|
-
let result = applyDynamicFilters(base);
|
|
117
|
-
|
|
118
|
-
// Search-miss fallback: drop search filter and notify consumer
|
|
119
|
-
if (result.length === 0 && activeSearchtext?.trim()) {
|
|
120
|
-
searchMissText = activeSearchtext.trim();
|
|
121
|
-
multipleMatchText = null;
|
|
122
|
-
result = applyDynamicFilters(base, { skipSearch: true });
|
|
123
|
-
onsearchmiss?.(activeSearchtext.trim());
|
|
124
|
-
} else {
|
|
125
|
-
searchMissText = null;
|
|
126
|
-
multipleMatchText = (result.length > 1 && activeSearchtext?.trim()) ? activeSearchtext.trim() : null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
filteredEntries = result;
|
|
130
|
-
|
|
131
|
-
// Auto-select / auto-clear only when dynamic filters are active
|
|
132
|
-
if (activeSearchtext?.trim() || activeDatasetfilter) {
|
|
133
|
-
if (result.length === 1 && result[0].id !== autoSelectedId) {
|
|
134
|
-
autoSelectedId = result[0].id;
|
|
135
|
-
select(result[0]);
|
|
136
|
-
} else if (result.length !== 1) {
|
|
137
|
-
autoSelectedId = null;
|
|
138
|
-
select(null);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
87
|
async function loadCatalogEntries() {
|
|
144
88
|
loading = true;
|
|
145
89
|
error = null;
|
|
@@ -160,13 +104,8 @@
|
|
|
160
104
|
|
|
161
105
|
const result = await client.catalogEntries.get(filters);
|
|
162
106
|
catalogEntries = result.items || [];
|
|
163
|
-
// Apply
|
|
164
|
-
filteredEntries =
|
|
165
|
-
|
|
166
|
-
// Re-apply dynamic filters if any are active
|
|
167
|
-
if (activeSearchtext || activeDatasetfilter) {
|
|
168
|
-
runFilters();
|
|
169
|
-
}
|
|
107
|
+
// Apply filters synchronously so initialselection works
|
|
108
|
+
filteredEntries = applyFilters(catalogEntries);
|
|
170
109
|
|
|
171
110
|
// Auto-select initial selection if provided
|
|
172
111
|
if (initialselection) {
|
|
@@ -177,7 +116,7 @@
|
|
|
177
116
|
}
|
|
178
117
|
}
|
|
179
118
|
|
|
180
|
-
onitemsloaded?.(filteredEntries
|
|
119
|
+
onitemsloaded?.(filteredEntries);
|
|
181
120
|
} catch (e) {
|
|
182
121
|
console.error('Failed to load catalog entries:', e);
|
|
183
122
|
error = e.message || 'Failed to load catalog entries';
|
|
@@ -199,29 +138,20 @@
|
|
|
199
138
|
}
|
|
200
139
|
}
|
|
201
140
|
|
|
202
|
-
//
|
|
203
|
-
export function
|
|
204
|
-
activeSearchtext = opts.searchtext ?? null;
|
|
205
|
-
activeDatasetfilter = opts.datasetfilter ?? null;
|
|
206
|
-
runFilters();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Programmatically select a catalog entry (pass null to clear)
|
|
210
|
-
export function select(idOrEntry: string | CatalogEntry | null) {
|
|
211
|
-
if (!idOrEntry) {
|
|
212
|
-
selectedValue = '';
|
|
213
|
-
if (dropdownRef) dropdownRef.value = '';
|
|
214
|
-
onentryselect?.(null);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
141
|
+
// Exposed method to programmatically select a catalog entry
|
|
142
|
+
export function select(idOrEntry: string | CatalogEntry) {
|
|
217
143
|
const id = typeof idOrEntry === 'string' ? idOrEntry : idOrEntry?.id;
|
|
218
144
|
if (!id) return;
|
|
219
145
|
|
|
220
146
|
const entry = filteredEntries.find(ce => ce.id === id);
|
|
221
147
|
if (entry) {
|
|
222
148
|
selectedValue = id;
|
|
223
|
-
if (dropdownRef)
|
|
224
|
-
|
|
149
|
+
if (dropdownRef) {
|
|
150
|
+
dropdownRef.value = id;
|
|
151
|
+
}
|
|
152
|
+
if (onentryselect) {
|
|
153
|
+
onentryselect(entry);
|
|
154
|
+
}
|
|
225
155
|
}
|
|
226
156
|
}
|
|
227
157
|
|
|
@@ -239,13 +169,6 @@
|
|
|
239
169
|
export function getAllEntries(): CatalogEntry[] {
|
|
240
170
|
return catalogEntries;
|
|
241
171
|
}
|
|
242
|
-
|
|
243
|
-
// Reload entries from the API, optionally auto-selecting an entry by ID after reload
|
|
244
|
-
export function reload(selectAfterReload?: string) {
|
|
245
|
-
loadCatalogEntries().then(() => {
|
|
246
|
-
if (selectAfterReload) select(selectAfterReload);
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
172
|
</script>
|
|
250
173
|
|
|
251
174
|
{#if loading}
|
|
@@ -285,42 +208,8 @@
|
|
|
285
208
|
</cds-dropdown-item>
|
|
286
209
|
{/each}
|
|
287
210
|
</cds-dropdown>
|
|
288
|
-
{#if searchMissText}
|
|
289
|
-
<div class="search-miss search-miss-muted">
|
|
290
|
-
<svg viewBox="0 0 32 32" fill="currentColor" width="14" height="14">
|
|
291
|
-
<path d="M16 2a14 14 0 1014 14A14 14 0 0016 2zm-1.13 5h2.25v12h-2.25zm1.13 17a1.5 1.5 0 111.5-1.5A1.5 1.5 0 0116 24z"/>
|
|
292
|
-
</svg>
|
|
293
|
-
<span>No match for "{searchMissText}" — showing all entries</span>
|
|
294
|
-
</div>
|
|
295
|
-
{/if}
|
|
296
|
-
{#if multipleMatchText}
|
|
297
|
-
<div class="search-miss search-miss-muted">
|
|
298
|
-
<svg viewBox="0 0 32 32" fill="currentColor" width="14" height="14">
|
|
299
|
-
<path d="M16 2a14 14 0 1014 14A14 14 0 0016 2zm-1.13 5h2.25v12h-2.25zm1.13 17a1.5 1.5 0 111.5-1.5A1.5 1.5 0 0116 24z"/>
|
|
300
|
-
</svg>
|
|
301
|
-
<span>Multiple entries found for "{multipleMatchText}" — please make a selection</span>
|
|
302
|
-
</div>
|
|
303
|
-
{/if}
|
|
304
211
|
{/if}
|
|
305
212
|
|
|
306
213
|
<style>
|
|
307
|
-
|
|
308
|
-
display: flex;
|
|
309
|
-
align-items: center;
|
|
310
|
-
gap: 0.5rem;
|
|
311
|
-
padding: 0.5rem 0.75rem;
|
|
312
|
-
margin-top: 0.25rem;
|
|
313
|
-
font-size: 0.875rem;
|
|
314
|
-
background: var(--cds-support-warning, #f1c21b);
|
|
315
|
-
color: #161616;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
.search-miss-muted {
|
|
319
|
-
background: var(--cds-layer-02, #e0e0e0);
|
|
320
|
-
color: var(--cds-text-secondary, #525252);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
.search-miss svg {
|
|
324
|
-
flex-shrink: 0;
|
|
325
|
-
}
|
|
214
|
+
/* Styles moved to inline for shadow DOM compatibility */
|
|
326
215
|
</style>
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { CatalogEntry, DataType } from '@industream/datacatalog-client/dto';
|
|
2
|
-
interface FilterOptions {
|
|
3
|
-
searchtext?: string | null;
|
|
4
|
-
datasetfilter?: string | null;
|
|
5
|
-
}
|
|
6
2
|
interface Props {
|
|
7
3
|
id?: string;
|
|
8
4
|
dcapiurl?: string;
|
|
@@ -10,17 +6,14 @@ interface Props {
|
|
|
10
6
|
datatypefilter?: DataType | DataType[] | null;
|
|
11
7
|
namefilter?: string | string[] | null;
|
|
12
8
|
initialselection?: string | null;
|
|
13
|
-
onentryselect?: (entry: CatalogEntry
|
|
14
|
-
onitemsloaded?: (
|
|
15
|
-
onsearchmiss?: (text: string) => void;
|
|
9
|
+
onentryselect?: (entry: CatalogEntry) => void;
|
|
10
|
+
onitemsloaded?: (entries: CatalogEntry[]) => void;
|
|
16
11
|
}
|
|
17
12
|
declare const DCCatalogEntry: import("svelte").Component<Props, {
|
|
18
|
-
|
|
19
|
-
select: (idOrEntry: string | CatalogEntry | null) => void;
|
|
13
|
+
select: (idOrEntry: string | CatalogEntry) => void;
|
|
20
14
|
getSelection: () => CatalogEntry | null;
|
|
21
15
|
getEntries: () => CatalogEntry[];
|
|
22
16
|
getAllEntries: () => CatalogEntry[];
|
|
23
|
-
reload: (selectAfterReload?: string) => void;
|
|
24
17
|
}, "">;
|
|
25
18
|
type DCCatalogEntry = ReturnType<typeof DCCatalogEntry>;
|
|
26
19
|
export default DCCatalogEntry;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DCCatalogEntryPicker — DataCatalog entry picker with configurable columns.
|
|
4
|
+
*
|
|
5
|
+
* Composes TagBrowser (searchable table with multi-select) and SelectedTags
|
|
6
|
+
* (collapsible summary with remove). Owns the DataCatalog fetch lifecycle.
|
|
7
|
+
*
|
|
8
|
+
* Column keys reference the COLUMN_DEFS registry in columns.ts:
|
|
9
|
+
* 'name' → CatalogEntry.name (text)
|
|
10
|
+
* 'dataType' → CatalogEntry.dataType (pill)
|
|
11
|
+
* 'nodeId' → CatalogEntry.sourceParams.nodeId (code)
|
|
12
|
+
* 'unit' → CatalogEntry.metadata.unit (pill)
|
|
13
|
+
* 'labels' → CatalogEntry.labels[] (label pills)
|
|
14
|
+
*
|
|
15
|
+
* @prop dcApiUrl — DataCatalog API base URL
|
|
16
|
+
* @prop sourceConnectionId — Filters catalog entries by source connection
|
|
17
|
+
* @prop selectedIds — (bindable) Array of selected CatalogEntry IDs
|
|
18
|
+
* @prop entries — (bindable) Loaded catalog entries (exposed for parent access)
|
|
19
|
+
* @prop columns — Column keys for the browse table (default: name, dataType, labels)
|
|
20
|
+
* @prop selectedColumnsDisplay — Column keys for the selected tags panel (default: name, dataType, labels)
|
|
21
|
+
* @prop emptyMessage — Message when no entries match the source connection
|
|
22
|
+
* @prop onSelectionChange — Called when selection changes (receives full selectedIds array)
|
|
23
|
+
* @prop onRemove — Called when a single entry is removed from the selection
|
|
24
|
+
*/
|
|
25
|
+
import { DataCatalogClient } from '@industream/datacatalog-client';
|
|
26
|
+
import type { CatalogEntry } from '@industream/datacatalog-client/dto';
|
|
27
|
+
import { DEFAULT_COLUMNS, DEFAULT_SELECTED_COLUMNS_DISPLAY } from './picker-column-helpers';
|
|
28
|
+
import TagBrowser from './TagBrowser.svelte';
|
|
29
|
+
import SelectedTags from './SelectedTags.svelte';
|
|
30
|
+
|
|
31
|
+
interface Props {
|
|
32
|
+
dcApiUrl?: string;
|
|
33
|
+
sourceConnectionId?: string;
|
|
34
|
+
selectedIds?: string[];
|
|
35
|
+
entries?: CatalogEntry[];
|
|
36
|
+
columns?: string[];
|
|
37
|
+
selectedColumnsDisplay?: string[];
|
|
38
|
+
emptyMessage?: string;
|
|
39
|
+
onSelectionChange?: (ids: string[]) => void;
|
|
40
|
+
onRemove?: (id: string) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
dcApiUrl = '',
|
|
45
|
+
sourceConnectionId = '',
|
|
46
|
+
selectedIds = $bindable([]),
|
|
47
|
+
entries = $bindable([]),
|
|
48
|
+
columns = DEFAULT_COLUMNS,
|
|
49
|
+
selectedColumnsDisplay = DEFAULT_SELECTED_COLUMNS_DISPLAY,
|
|
50
|
+
emptyMessage = 'No entries found for this source connection.',
|
|
51
|
+
onSelectionChange = null,
|
|
52
|
+
onRemove = null
|
|
53
|
+
}: Props = $props();
|
|
54
|
+
|
|
55
|
+
let loading = $state(false);
|
|
56
|
+
let error = $state('');
|
|
57
|
+
|
|
58
|
+
$effect(() => {
|
|
59
|
+
if (dcApiUrl && sourceConnectionId) {
|
|
60
|
+
loadEntries(dcApiUrl, sourceConnectionId);
|
|
61
|
+
} else {
|
|
62
|
+
entries = [];
|
|
63
|
+
error = '';
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
async function loadEntries(apiUrl: string, connId: string) {
|
|
68
|
+
loading = true;
|
|
69
|
+
error = '';
|
|
70
|
+
try {
|
|
71
|
+
const client = new DataCatalogClient({ baseUrl: apiUrl });
|
|
72
|
+
const result = await client.catalogEntries.get({ sourceConnectionIds: [connId] });
|
|
73
|
+
entries = result.items ?? [];
|
|
74
|
+
} catch (err: any) {
|
|
75
|
+
error = err.message ?? 'Failed to load catalog entries';
|
|
76
|
+
entries = [];
|
|
77
|
+
} finally {
|
|
78
|
+
loading = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<TagBrowser
|
|
84
|
+
{entries}
|
|
85
|
+
bind:selectedIds
|
|
86
|
+
{columns}
|
|
87
|
+
{loading}
|
|
88
|
+
{error}
|
|
89
|
+
{emptyMessage}
|
|
90
|
+
{onSelectionChange}
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
{#if selectedIds.length > 0}
|
|
94
|
+
<SelectedTags
|
|
95
|
+
{entries}
|
|
96
|
+
bind:selectedIds
|
|
97
|
+
columns={selectedColumnsDisplay}
|
|
98
|
+
{onRemove}
|
|
99
|
+
/>
|
|
100
|
+
{:else}
|
|
101
|
+
<small class="helper-text warning">Select at least one tag to monitor</small>
|
|
102
|
+
{/if}
|
|
103
|
+
|
|
104
|
+
<style>
|
|
105
|
+
.helper-text {
|
|
106
|
+
color: var(--cds-text-secondary, #525252);
|
|
107
|
+
font-size: 0.75rem;
|
|
108
|
+
margin-top: 0.25rem;
|
|
109
|
+
}
|
|
110
|
+
.helper-text.warning {
|
|
111
|
+
color: var(--cds-support-warning, #f1c21b);
|
|
112
|
+
}
|
|
113
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CatalogEntry } from '@industream/datacatalog-client/dto';
|
|
2
|
+
interface Props {
|
|
3
|
+
dcApiUrl?: string;
|
|
4
|
+
sourceConnectionId?: string;
|
|
5
|
+
selectedIds?: string[];
|
|
6
|
+
entries?: CatalogEntry[];
|
|
7
|
+
columns?: string[];
|
|
8
|
+
selectedColumnsDisplay?: string[];
|
|
9
|
+
emptyMessage?: string;
|
|
10
|
+
onSelectionChange?: (ids: string[]) => void;
|
|
11
|
+
onRemove?: (id: string) => void;
|
|
12
|
+
}
|
|
13
|
+
declare const DCCatalogEntryPicker: import("svelte").Component<Props, {}, "entries" | "selectedIds">;
|
|
14
|
+
type DCCatalogEntryPicker = ReturnType<typeof DCCatalogEntryPicker>;
|
|
15
|
+
export default DCCatalogEntryPicker;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { DEFAULT_SELECTED_COLUMNS_DISPLAY, resolveColumn, resolveValue } from './picker-column-helpers';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
entries = [],
|
|
6
|
+
selectedIds = $bindable([]),
|
|
7
|
+
columns = DEFAULT_SELECTED_COLUMNS_DISPLAY,
|
|
8
|
+
onRemove = null
|
|
9
|
+
} = $props();
|
|
10
|
+
|
|
11
|
+
let expanded = $state(true);
|
|
12
|
+
|
|
13
|
+
/** Resolved column descriptors — maps each column key to its label, resolver and render type */
|
|
14
|
+
const columnDescriptors = $derived(
|
|
15
|
+
columns
|
|
16
|
+
.map(k => { const resolved = resolveColumn(k); return resolved ? { key: k, ...resolved } : null; })
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const selectedEntries = $derived(() => {
|
|
21
|
+
const idSet = new Set(selectedIds);
|
|
22
|
+
return entries.filter(e => idSet.has(e.id));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const selectedCount = $derived(selectedIds.length);
|
|
26
|
+
|
|
27
|
+
$effect(() => {
|
|
28
|
+
if (selectedCount === 0) {
|
|
29
|
+
expanded = false;
|
|
30
|
+
} else if (selectedCount > 0 && !expanded) {
|
|
31
|
+
expanded = true;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function handleRemove(id) {
|
|
36
|
+
onRemove?.(id);
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div class="selected-tags">
|
|
41
|
+
<button class="panel-header" aria-expanded={expanded} onclick={() => expanded = !expanded}>
|
|
42
|
+
<svg class="chevron" class:chevron-expanded={expanded} viewBox="0 0 16 16" width="16" height="16">
|
|
43
|
+
<path d="M11 8L6 13V3z" fill="currentColor"></path>
|
|
44
|
+
</svg>
|
|
45
|
+
<span class="panel-title">Selected Tags ({selectedCount})</span>
|
|
46
|
+
</button>
|
|
47
|
+
|
|
48
|
+
{#if expanded}
|
|
49
|
+
<div class="panel-body">
|
|
50
|
+
{#if selectedEntries().length === 0}
|
|
51
|
+
<div class="empty-message">No tags selected</div>
|
|
52
|
+
{:else}
|
|
53
|
+
<div class="tag-list">
|
|
54
|
+
{#each selectedEntries() as entry (entry.id)}
|
|
55
|
+
<div class="tag-row">
|
|
56
|
+
<div class="tag-info">
|
|
57
|
+
{#each columnDescriptors as columnDescriptor, index (columnDescriptor.key)}
|
|
58
|
+
{@const val = resolveValue(entry, columnDescriptor.key)}
|
|
59
|
+
{#if index === 0}
|
|
60
|
+
<!-- First column: bold primary label -->
|
|
61
|
+
<span class="tag-primary">{val ?? '-'}</span>
|
|
62
|
+
{:else if columnDescriptor.type === 'labels' && Array.isArray(val)}
|
|
63
|
+
{#each val as label (label.id)}
|
|
64
|
+
<span class="meta-item">{label.name}</span>
|
|
65
|
+
{/each}
|
|
66
|
+
{:else if columnDescriptor.type === 'code'}
|
|
67
|
+
{#if val}<code class="tag-code">{val}</code>{/if}
|
|
68
|
+
{:else}
|
|
69
|
+
{#if val}<span class="meta-item">{val}</span>{/if}
|
|
70
|
+
{/if}
|
|
71
|
+
{/each}
|
|
72
|
+
</div>
|
|
73
|
+
<button
|
|
74
|
+
class="remove-button"
|
|
75
|
+
title="Remove {entry.name}"
|
|
76
|
+
aria-label="Remove {entry.name}"
|
|
77
|
+
onclick={() => handleRemove(entry.id)}
|
|
78
|
+
>
|
|
79
|
+
<svg viewBox="0 0 16 16" width="14" height="14"><path d="M12 4.7L11.3 4 8 7.3 4.7 4 4 4.7 7.3 8 4 11.3l.7.7L8 8.7l3.3 3.3.7-.7L8.7 8z" fill="currentColor"></path></svg>
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
{/each}
|
|
83
|
+
</div>
|
|
84
|
+
{/if}
|
|
85
|
+
</div>
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<style>
|
|
90
|
+
.selected-tags {
|
|
91
|
+
border: 1px solid var(--cds-border-subtle-01, #393939);
|
|
92
|
+
background: var(--cds-layer-01, #262626);
|
|
93
|
+
}
|
|
94
|
+
.panel-header {
|
|
95
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
96
|
+
width: 100%; padding: 0.5rem 0.75rem;
|
|
97
|
+
background: var(--cds-layer-02, #2e2e2e); border: none;
|
|
98
|
+
color: var(--cds-text-primary, #f4f4f4);
|
|
99
|
+
font-size: 0.8125rem; font-weight: 600;
|
|
100
|
+
cursor: pointer; text-align: left; transition: background-color 0.15s;
|
|
101
|
+
}
|
|
102
|
+
.panel-header:hover { background: var(--cds-layer-hover-01, #4c4c4c); }
|
|
103
|
+
.chevron {
|
|
104
|
+
color: var(--cds-text-secondary, #c6c6c6);
|
|
105
|
+
transition: transform 0.15s ease; flex-shrink: 0;
|
|
106
|
+
}
|
|
107
|
+
.chevron-expanded { transform: rotate(90deg); }
|
|
108
|
+
.panel-title { flex: 1; }
|
|
109
|
+
.panel-body { max-height: 200px; overflow-y: auto; }
|
|
110
|
+
.empty-message {
|
|
111
|
+
padding: 1rem; text-align: center;
|
|
112
|
+
color: var(--cds-text-helper, #8d8d8d); font-size: 0.8125rem;
|
|
113
|
+
}
|
|
114
|
+
.tag-row {
|
|
115
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
116
|
+
padding: 0.375rem 0.75rem;
|
|
117
|
+
border-bottom: 1px solid var(--cds-border-subtle-01, #393939);
|
|
118
|
+
transition: background-color 0.1s;
|
|
119
|
+
}
|
|
120
|
+
.tag-row:last-child { border-bottom: none; }
|
|
121
|
+
.tag-row:hover { background: var(--cds-layer-hover-01, #4c4c4c); }
|
|
122
|
+
.tag-info {
|
|
123
|
+
flex: 1; display: flex; align-items: center; gap: 0.75rem;
|
|
124
|
+
min-width: 0; overflow: hidden;
|
|
125
|
+
}
|
|
126
|
+
.tag-primary {
|
|
127
|
+
font-weight: 600; font-size: 0.8125rem;
|
|
128
|
+
color: var(--cds-text-primary, #f4f4f4);
|
|
129
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
130
|
+
flex-shrink: 0; max-width: 12rem;
|
|
131
|
+
}
|
|
132
|
+
.tag-code {
|
|
133
|
+
font-family: 'IBM Plex Mono', monospace; font-size: 0.6875rem;
|
|
134
|
+
color: var(--cds-text-secondary, #c6c6c6);
|
|
135
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
|
|
136
|
+
}
|
|
137
|
+
.meta-item {
|
|
138
|
+
padding: 0.0625rem 0.375rem;
|
|
139
|
+
background: var(--cds-tag-background-gray, #393939);
|
|
140
|
+
color: var(--cds-tag-color-gray, #c6c6c6);
|
|
141
|
+
font-size: 0.6875rem; border-radius: 1rem;
|
|
142
|
+
}
|
|
143
|
+
.remove-button {
|
|
144
|
+
background: transparent; border: none;
|
|
145
|
+
color: var(--cds-text-secondary, #c6c6c6);
|
|
146
|
+
cursor: pointer; padding: 0.25rem;
|
|
147
|
+
transition: color 0.15s; flex-shrink: 0;
|
|
148
|
+
}
|
|
149
|
+
.remove-button:hover { color: var(--cds-support-error, #fa4d56); }
|
|
150
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default SelectedTags;
|
|
2
|
+
type SelectedTags = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const SelectedTags: import("svelte").Component<{
|
|
7
|
+
entries?: any[];
|
|
8
|
+
selectedIds?: any[];
|
|
9
|
+
columns?: typeof DEFAULT_SELECTED_COLUMNS_DISPLAY;
|
|
10
|
+
onRemove?: any;
|
|
11
|
+
}, {}, "selectedIds">;
|
|
12
|
+
type $$ComponentProps = {
|
|
13
|
+
entries?: any[];
|
|
14
|
+
selectedIds?: any[];
|
|
15
|
+
columns?: typeof DEFAULT_SELECTED_COLUMNS_DISPLAY;
|
|
16
|
+
onRemove?: any;
|
|
17
|
+
};
|
|
18
|
+
import { DEFAULT_SELECTED_COLUMNS_DISPLAY } from './picker-column-helpers';
|