@industream/flowmaker-flowbox-ui-components 1.0.5 → 1.0.6-poc.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.
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* @prop onSelectionChange — Called when selection changes (receives full selectedIds array)
|
|
25
25
|
* @prop onRemove — Called when a single entry is removed from the selection
|
|
26
26
|
*/
|
|
27
|
+
import { untrack } from 'svelte';
|
|
27
28
|
import { DataCatalogClient } from '@industream/datacatalog-client';
|
|
28
29
|
import type { CatalogEntry } from '@industream/datacatalog-client/dto';
|
|
29
30
|
import { DEFAULT_COLUMNS, DEFAULT_SELECTED_COLUMNS_DISPLAY } from './picker-column-helpers';
|
|
@@ -126,9 +127,11 @@
|
|
|
126
127
|
const uniqueItems = deduplicateEntries(items);
|
|
127
128
|
|
|
128
129
|
if (uniqueItems.length > MAX_ENTRIES) {
|
|
129
|
-
// Too many
|
|
130
|
+
// Too many for client-side filtering → server-search mode. Still show the
|
|
131
|
+
// first MAX_ENTRIES as a browsable PREVIEW (with a "not all displayed"
|
|
132
|
+
// warning in TagBrowser) instead of a blank "type to search" screen.
|
|
130
133
|
tooMany = true;
|
|
131
|
-
entries =
|
|
134
|
+
entries = uniqueItems.slice(0, MAX_ENTRIES);
|
|
132
135
|
} else {
|
|
133
136
|
tooMany = false;
|
|
134
137
|
entries = uniqueItems;
|
|
@@ -152,9 +155,9 @@
|
|
|
152
155
|
searchAbort?.abort();
|
|
153
156
|
|
|
154
157
|
if (!query) {
|
|
155
|
-
// Empty query in too-many mode →
|
|
156
|
-
entries = [];
|
|
158
|
+
// Empty query in too-many mode → restore the first-N preview.
|
|
157
159
|
searching = false;
|
|
160
|
+
loadInitial(dcApiUrl, sourceConnectionId);
|
|
158
161
|
return;
|
|
159
162
|
}
|
|
160
163
|
|
|
@@ -180,6 +183,48 @@
|
|
|
180
183
|
searching = false;
|
|
181
184
|
});
|
|
182
185
|
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Full details of the SELECTED entries, fetched by ID — decoupled from the browse
|
|
189
|
+
* list (`entries`), which is empty in `tooMany` mode and only holds the current
|
|
190
|
+
* page/search otherwise. Without this, the SelectedTags panel can only render
|
|
191
|
+
* selected tags that happen to be in `entries` → on large sources it shows
|
|
192
|
+
* "No tags selected" / loses the selection visually despite a correct count.
|
|
193
|
+
*/
|
|
194
|
+
let selectedEntries = $state<CatalogEntry[]>([]);
|
|
195
|
+
|
|
196
|
+
$effect(() => {
|
|
197
|
+
const ids = [...selectedIds]; // reactive dependency
|
|
198
|
+
const url = dcApiUrl; // reactive dependency
|
|
199
|
+
|
|
200
|
+
if (!url || ids.length === 0) {
|
|
201
|
+
selectedEntries = [];
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Reuse already-resolved entries that are still selected; fetch only the missing.
|
|
206
|
+
const idSet = new Set(ids);
|
|
207
|
+
const current = untrack(() => selectedEntries);
|
|
208
|
+
const keep = current.filter((e) => idSet.has(e.id));
|
|
209
|
+
const have = new Set(keep.map((e) => e.id));
|
|
210
|
+
const missing = ids.filter((id) => !have.has(id));
|
|
211
|
+
|
|
212
|
+
if (missing.length === 0) {
|
|
213
|
+
if (keep.length !== current.length) selectedEntries = keep;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// get() auto-switches to POST /catalog-entries/query for large id lists (URI-safe).
|
|
218
|
+
getClient(url).catalogEntries
|
|
219
|
+
.get({ ids: missing })
|
|
220
|
+
.then((res) => {
|
|
221
|
+
const latest = new Set(untrack(() => selectedIds));
|
|
222
|
+
selectedEntries = [...keep, ...(res.items ?? [])].filter((e) => latest.has(e.id));
|
|
223
|
+
})
|
|
224
|
+
.catch(() => {
|
|
225
|
+
// Keep what we have; SelectedTags falls back to the browse list below.
|
|
226
|
+
});
|
|
227
|
+
});
|
|
183
228
|
</script>
|
|
184
229
|
|
|
185
230
|
<TagBrowser
|
|
@@ -197,7 +242,7 @@
|
|
|
197
242
|
|
|
198
243
|
{#if selectedIds.length > 0}
|
|
199
244
|
<SelectedTags
|
|
200
|
-
{entries}
|
|
245
|
+
entries={selectedEntries.length > 0 ? selectedEntries : entries}
|
|
201
246
|
bind:selectedIds
|
|
202
247
|
columns={selectedColumnsDisplay}
|
|
203
248
|
{onRemove}
|
|
@@ -39,9 +39,6 @@
|
|
|
39
39
|
const totalCount = $derived(entries.length);
|
|
40
40
|
const allVisibleSelected = $derived(filtered().length > 0 && filtered().every(e => selectedIds.includes(e.id)));
|
|
41
41
|
|
|
42
|
-
/** Whether to show the "too many entries" hint instead of the table */
|
|
43
|
-
const showTooManyHint = $derived(tooMany && !searchQuery.trim());
|
|
44
|
-
|
|
45
42
|
function handleSearchInput(e) {
|
|
46
43
|
searchQuery = e.target.value;
|
|
47
44
|
if (!onSearch) return;
|
|
@@ -109,14 +106,15 @@
|
|
|
109
106
|
<svg viewBox="0 0 16 16" width="16" height="16"><path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm-.75 3.5h1.5v5h-1.5zm.75 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" fill="currentColor"></path></svg>
|
|
110
107
|
<span>{error}</span>
|
|
111
108
|
</div>
|
|
112
|
-
{:else if showTooManyHint}
|
|
113
|
-
<div class="state-message too-many-state">
|
|
114
|
-
<svg viewBox="0 0 16 16" width="16" height="16"><path d="M15.5 13.586L11.742 9.828A5.514 5.514 0 0 0 13 6.5 5.506 5.506 0 0 0 7.5 1 5.506 5.506 0 0 0 2 6.5 5.506 5.506 0 0 0 7.5 12a5.514 5.514 0 0 0 3.328-1.258l3.758 3.758zM3 6.5A4.505 4.505 0 0 1 7.5 2 4.505 4.505 0 0 1 12 6.5 4.505 4.505 0 0 1 7.5 11 4.505 4.505 0 0 1 3 6.5z" fill="currentColor"></path></svg>
|
|
115
|
-
<span>Too many entries. Type in the search box to refine.</span>
|
|
116
|
-
</div>
|
|
117
109
|
{:else if entries.length === 0 && !searching}
|
|
118
|
-
<div class="state-message empty-state"><span>{emptyMessage}</span></div>
|
|
110
|
+
<div class="state-message empty-state"><span>{tooMany ? 'No matching entries. Refine your search.' : emptyMessage}</span></div>
|
|
119
111
|
{:else}
|
|
112
|
+
{#if tooMany}
|
|
113
|
+
<div class="too-many-banner">
|
|
114
|
+
<svg viewBox="0 0 16 16" width="16" height="16"><path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm-.75 3.5h1.5v5h-1.5zm.75 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" fill="currentColor"></path></svg>
|
|
115
|
+
<span>Showing {entries.length} entries (not all). Use the search box to find others.</span>
|
|
116
|
+
</div>
|
|
117
|
+
{/if}
|
|
120
118
|
<div class="table-container">
|
|
121
119
|
<table class="entry-table">
|
|
122
120
|
<!-- Table headers: one checkbox column + one column per columnDescriptor -->
|
|
@@ -203,7 +201,14 @@
|
|
|
203
201
|
}
|
|
204
202
|
.error-state { color: var(--cds-support-error, #fa4d56); }
|
|
205
203
|
.empty-state { color: var(--cds-text-helper, #8d8d8d); }
|
|
206
|
-
.too-many-
|
|
204
|
+
.too-many-banner {
|
|
205
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
206
|
+
padding: 0.5rem 0.75rem;
|
|
207
|
+
background: var(--cds-notification-background-warning, #3d3522);
|
|
208
|
+
border-left: 3px solid var(--cds-support-warning, #f1c21b);
|
|
209
|
+
color: var(--cds-text-primary, #f4f4f4); font-size: 0.75rem;
|
|
210
|
+
}
|
|
211
|
+
.too-many-banner svg { color: var(--cds-support-warning, #f1c21b); flex: 0 0 auto; }
|
|
207
212
|
.spinner {
|
|
208
213
|
width: 1rem; height: 1rem;
|
|
209
214
|
border: 2px solid var(--cds-border-subtle-01, #e0e0e0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@industream/flowmaker-flowbox-ui-components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6-poc.1",
|
|
4
4
|
"description": "Reusable Svelte components for FlowMaker FlowBox UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./dist/index.js",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"README.md"
|
|
40
40
|
],
|
|
41
41
|
"scripts": {
|
|
42
|
-
"build": "svelte-package --input src"
|
|
42
|
+
"build": "svelte-package --input src",
|
|
43
|
+
"dev": "vite --config vite.dev.config.ts"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
46
|
"svelte": "^5.0.0",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* @prop onSelectionChange — Called when selection changes (receives full selectedIds array)
|
|
25
25
|
* @prop onRemove — Called when a single entry is removed from the selection
|
|
26
26
|
*/
|
|
27
|
+
import { untrack } from 'svelte';
|
|
27
28
|
import { DataCatalogClient } from '@industream/datacatalog-client';
|
|
28
29
|
import type { CatalogEntry } from '@industream/datacatalog-client/dto';
|
|
29
30
|
import { DEFAULT_COLUMNS, DEFAULT_SELECTED_COLUMNS_DISPLAY } from './picker-column-helpers';
|
|
@@ -126,9 +127,11 @@
|
|
|
126
127
|
const uniqueItems = deduplicateEntries(items);
|
|
127
128
|
|
|
128
129
|
if (uniqueItems.length > MAX_ENTRIES) {
|
|
129
|
-
// Too many
|
|
130
|
+
// Too many for client-side filtering → server-search mode. Still show the
|
|
131
|
+
// first MAX_ENTRIES as a browsable PREVIEW (with a "not all displayed"
|
|
132
|
+
// warning in TagBrowser) instead of a blank "type to search" screen.
|
|
130
133
|
tooMany = true;
|
|
131
|
-
entries =
|
|
134
|
+
entries = uniqueItems.slice(0, MAX_ENTRIES);
|
|
132
135
|
} else {
|
|
133
136
|
tooMany = false;
|
|
134
137
|
entries = uniqueItems;
|
|
@@ -152,9 +155,9 @@
|
|
|
152
155
|
searchAbort?.abort();
|
|
153
156
|
|
|
154
157
|
if (!query) {
|
|
155
|
-
// Empty query in too-many mode →
|
|
156
|
-
entries = [];
|
|
158
|
+
// Empty query in too-many mode → restore the first-N preview.
|
|
157
159
|
searching = false;
|
|
160
|
+
loadInitial(dcApiUrl, sourceConnectionId);
|
|
158
161
|
return;
|
|
159
162
|
}
|
|
160
163
|
|
|
@@ -180,6 +183,48 @@
|
|
|
180
183
|
searching = false;
|
|
181
184
|
});
|
|
182
185
|
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Full details of the SELECTED entries, fetched by ID — decoupled from the browse
|
|
189
|
+
* list (`entries`), which is empty in `tooMany` mode and only holds the current
|
|
190
|
+
* page/search otherwise. Without this, the SelectedTags panel can only render
|
|
191
|
+
* selected tags that happen to be in `entries` → on large sources it shows
|
|
192
|
+
* "No tags selected" / loses the selection visually despite a correct count.
|
|
193
|
+
*/
|
|
194
|
+
let selectedEntries = $state<CatalogEntry[]>([]);
|
|
195
|
+
|
|
196
|
+
$effect(() => {
|
|
197
|
+
const ids = [...selectedIds]; // reactive dependency
|
|
198
|
+
const url = dcApiUrl; // reactive dependency
|
|
199
|
+
|
|
200
|
+
if (!url || ids.length === 0) {
|
|
201
|
+
selectedEntries = [];
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Reuse already-resolved entries that are still selected; fetch only the missing.
|
|
206
|
+
const idSet = new Set(ids);
|
|
207
|
+
const current = untrack(() => selectedEntries);
|
|
208
|
+
const keep = current.filter((e) => idSet.has(e.id));
|
|
209
|
+
const have = new Set(keep.map((e) => e.id));
|
|
210
|
+
const missing = ids.filter((id) => !have.has(id));
|
|
211
|
+
|
|
212
|
+
if (missing.length === 0) {
|
|
213
|
+
if (keep.length !== current.length) selectedEntries = keep;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// get() auto-switches to POST /catalog-entries/query for large id lists (URI-safe).
|
|
218
|
+
getClient(url).catalogEntries
|
|
219
|
+
.get({ ids: missing })
|
|
220
|
+
.then((res) => {
|
|
221
|
+
const latest = new Set(untrack(() => selectedIds));
|
|
222
|
+
selectedEntries = [...keep, ...(res.items ?? [])].filter((e) => latest.has(e.id));
|
|
223
|
+
})
|
|
224
|
+
.catch(() => {
|
|
225
|
+
// Keep what we have; SelectedTags falls back to the browse list below.
|
|
226
|
+
});
|
|
227
|
+
});
|
|
183
228
|
</script>
|
|
184
229
|
|
|
185
230
|
<TagBrowser
|
|
@@ -197,7 +242,7 @@
|
|
|
197
242
|
|
|
198
243
|
{#if selectedIds.length > 0}
|
|
199
244
|
<SelectedTags
|
|
200
|
-
{entries}
|
|
245
|
+
entries={selectedEntries.length > 0 ? selectedEntries : entries}
|
|
201
246
|
bind:selectedIds
|
|
202
247
|
columns={selectedColumnsDisplay}
|
|
203
248
|
{onRemove}
|
|
@@ -39,9 +39,6 @@
|
|
|
39
39
|
const totalCount = $derived(entries.length);
|
|
40
40
|
const allVisibleSelected = $derived(filtered().length > 0 && filtered().every(e => selectedIds.includes(e.id)));
|
|
41
41
|
|
|
42
|
-
/** Whether to show the "too many entries" hint instead of the table */
|
|
43
|
-
const showTooManyHint = $derived(tooMany && !searchQuery.trim());
|
|
44
|
-
|
|
45
42
|
function handleSearchInput(e) {
|
|
46
43
|
searchQuery = e.target.value;
|
|
47
44
|
if (!onSearch) return;
|
|
@@ -109,14 +106,15 @@
|
|
|
109
106
|
<svg viewBox="0 0 16 16" width="16" height="16"><path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm-.75 3.5h1.5v5h-1.5zm.75 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" fill="currentColor"></path></svg>
|
|
110
107
|
<span>{error}</span>
|
|
111
108
|
</div>
|
|
112
|
-
{:else if showTooManyHint}
|
|
113
|
-
<div class="state-message too-many-state">
|
|
114
|
-
<svg viewBox="0 0 16 16" width="16" height="16"><path d="M15.5 13.586L11.742 9.828A5.514 5.514 0 0 0 13 6.5 5.506 5.506 0 0 0 7.5 1 5.506 5.506 0 0 0 2 6.5 5.506 5.506 0 0 0 7.5 12a5.514 5.514 0 0 0 3.328-1.258l3.758 3.758zM3 6.5A4.505 4.505 0 0 1 7.5 2 4.505 4.505 0 0 1 12 6.5 4.505 4.505 0 0 1 7.5 11 4.505 4.505 0 0 1 3 6.5z" fill="currentColor"></path></svg>
|
|
115
|
-
<span>Too many entries. Type in the search box to refine.</span>
|
|
116
|
-
</div>
|
|
117
109
|
{:else if entries.length === 0 && !searching}
|
|
118
|
-
<div class="state-message empty-state"><span>{emptyMessage}</span></div>
|
|
110
|
+
<div class="state-message empty-state"><span>{tooMany ? 'No matching entries. Refine your search.' : emptyMessage}</span></div>
|
|
119
111
|
{:else}
|
|
112
|
+
{#if tooMany}
|
|
113
|
+
<div class="too-many-banner">
|
|
114
|
+
<svg viewBox="0 0 16 16" width="16" height="16"><path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm-.75 3.5h1.5v5h-1.5zm.75 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" fill="currentColor"></path></svg>
|
|
115
|
+
<span>Showing {entries.length} entries (not all). Use the search box to find others.</span>
|
|
116
|
+
</div>
|
|
117
|
+
{/if}
|
|
120
118
|
<div class="table-container">
|
|
121
119
|
<table class="entry-table">
|
|
122
120
|
<!-- Table headers: one checkbox column + one column per columnDescriptor -->
|
|
@@ -203,7 +201,14 @@
|
|
|
203
201
|
}
|
|
204
202
|
.error-state { color: var(--cds-support-error, #fa4d56); }
|
|
205
203
|
.empty-state { color: var(--cds-text-helper, #8d8d8d); }
|
|
206
|
-
.too-many-
|
|
204
|
+
.too-many-banner {
|
|
205
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
206
|
+
padding: 0.5rem 0.75rem;
|
|
207
|
+
background: var(--cds-notification-background-warning, #3d3522);
|
|
208
|
+
border-left: 3px solid var(--cds-support-warning, #f1c21b);
|
|
209
|
+
color: var(--cds-text-primary, #f4f4f4); font-size: 0.75rem;
|
|
210
|
+
}
|
|
211
|
+
.too-many-banner svg { color: var(--cds-support-warning, #f1c21b); flex: 0 0 auto; }
|
|
207
212
|
.spinner {
|
|
208
213
|
width: 1rem; height: 1rem;
|
|
209
214
|
border: 2px solid var(--cds-border-subtle-01, #e0e0e0);
|