@industream/flowmaker-flowbox-ui-components 1.0.6-poc.1 → 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.
@@ -2,37 +2,46 @@
2
2
  /**
3
3
  * DCCatalogEntryPicker — DataCatalog entry picker with configurable columns.
4
4
  *
5
- * Composes TagBrowser (searchable table with multi-select) and SelectedTags
6
- * (collapsible summary with remove). Owns the DataCatalog fetch lifecycle.
5
+ * Composes TagBrowser (searchable, paginated table with multi-select) and
6
+ * SelectedTags (collapsible summary with remove). Owns the DataCatalog fetch
7
+ * lifecycle, pagination state, and the selected-entry metadata cache.
7
8
  *
8
- * Uses `client.catalogEntries.search()` (v1.4.0+) for both initial load and
9
- * server-side search. Filters by `sourceConnections` to scope entries.
10
- *
11
- * Load strategy:
12
- * 1. Initial load fetches up to MAX_ENTRIES + 1 items.
13
- * 2. If MAX_ENTRIES client mode (client-side search in TagBrowser).
14
- * 3. If > MAX_ENTRIES → server mode (TagBrowser search triggers server search
15
- * with 1s debounce, AbortSignal cancels in-flight requests).
9
+ * Strategy (see docs/superpowers/specs/2026-06-30-tag-picker-lazy-pagination-design.md):
10
+ * - Always load page 1 on open — never a blank table.
11
+ * - Lazy "load more" pagination via offset; totalCount drives the counter and end.
12
+ * - Search: client-side instant when the whole catalog is loaded (small catalogs),
13
+ * debounced server search otherwise.
14
+ * - Selected entries are cached independently of the loaded window so their
15
+ * metadata always renders.
16
16
  *
17
17
  * @prop dcApiUrl — DataCatalog API base URL
18
18
  * @prop sourceConnectionId — Filters catalog entries by source connection
19
19
  * @prop selectedIds — (bindable) Array of selected CatalogEntry IDs
20
20
  * @prop entries — (bindable) Loaded catalog entries (exposed for parent access)
21
- * @prop columns — Column keys for the browse table (default: name, dataType, labels)
22
- * @prop selectedColumnsDisplay — Column keys for the selected tags panel (default: name, dataType, labels)
21
+ * @prop columns — Column keys for the browse table
22
+ * @prop selectedColumnsDisplay — Column keys for the selected tags panel
23
23
  * @prop emptyMessage — Message when no entries match the source connection
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';
28
27
  import { DataCatalogClient } from '@industream/datacatalog-client';
29
28
  import type { CatalogEntry } from '@industream/datacatalog-client/dto';
30
29
  import { DEFAULT_COLUMNS, DEFAULT_SELECTED_COLUMNS_DISPLAY } from './picker-column-helpers';
30
+ import {
31
+ PAGE_SIZE,
32
+ MATCH_ALL,
33
+ mergePage,
34
+ deduplicateEntries,
35
+ normalizeQuery,
36
+ isAllLoaded,
37
+ hasMore,
38
+ remainingCount,
39
+ upsertSelectedCache,
40
+ resolveSelected
41
+ } from './picker-pagination';
31
42
  import TagBrowser from './TagBrowser.svelte';
32
43
  import SelectedTags from './SelectedTags.svelte';
33
44
 
34
- const MAX_ENTRIES = 100;
35
-
36
45
  interface Props {
37
46
  dcApiUrl?: string;
38
47
  sourceConnectionId?: string;
@@ -58,33 +67,46 @@
58
67
  }: Props = $props();
59
68
 
60
69
  let loading = $state(false);
70
+ let loadingMore = $state(false);
61
71
  let searching = $state(false);
62
72
  let error = $state('');
63
- let tooMany = $state(false);
73
+ let loadMoreError = $state('');
74
+ let totalCount = $state(0);
75
+ let currentQuery = $state(MATCH_ALL);
76
+ let offset = $state(0);
64
77
 
65
- /** Abort controller for cancelling in-flight search requests */
66
- let searchAbort: AbortController | null = null;
67
-
68
- /** Cached client instance recreated when dcApiUrl changes */
69
- let client: DataCatalogClient | null = null;
70
- let clientUrl = '';
78
+ /**
79
+ * Metadata for selected tags, kept across reloads/searches so SelectedTags always
80
+ * renders. Invariant: every mutation of this Map is paired with an `entries`
81
+ * reassignment, so the `selectedEntries` derived (which reads both) re-runs and picks
82
+ * up the change. Do NOT upsert the cache without also reassigning `entries`.
83
+ */
84
+ let selectedCache = new Map<string, CatalogEntry>();
71
85
 
72
86
  /**
73
- * Deduplicate entries by id keep only the first occurrence.
87
+ * Best-effort total for the current query. The server's search response MAY omit
88
+ * `totalCount`; when it does, we must not silently cap pagination at one page.
89
+ * Fall back to: a full page → "at least one more page" (loaded + PAGE_SIZE) so the
90
+ * load-more affordance stays available; a short page → exactly what is loaded (the end).
74
91
  */
75
- function deduplicateEntries(items: CatalogEntry[]): CatalogEntry[] {
76
- const seen = new Set<string>();
77
- const result: CatalogEntry[] = [];
78
- for (const item of items) {
79
- const id = item.id;
80
- if (!seen.has(id)) {
81
- seen.add(id);
82
- result.push(item);
83
- }
84
- }
85
- return result;
92
+ function estimateTotal(reported: number | undefined, loadedCount: number, lastPageLen: number): number {
93
+ if (reported != null) return reported;
94
+ return lastPageLen < PAGE_SIZE ? loadedCount : loadedCount + PAGE_SIZE;
86
95
  }
87
96
 
97
+ /** Abort controller for cancelling in-flight requests. */
98
+ let fetchAbort: AbortController | null = null;
99
+
100
+ /** Cached client instance — recreated when dcApiUrl changes. */
101
+ let client: DataCatalogClient | null = null;
102
+ let clientUrl = '';
103
+
104
+ /** Whether search runs server-side (large catalog) or client-side (everything loaded). */
105
+ const serverSearch = $derived(!isAllLoaded(MATCH_ALL, entries.length, totalCount) || currentQuery !== MATCH_ALL);
106
+
107
+ /** Resolved entries for the selected-tags panel: window first, cache fallback. */
108
+ const selectedEntries = $derived(resolveSelected(entries, selectedCache, selectedIds));
109
+
88
110
  function getClient(apiUrl: string): DataCatalogClient {
89
111
  if (client && clientUrl === apiUrl) return client;
90
112
  client = new DataCatalogClient({ baseUrl: apiUrl });
@@ -96,135 +118,124 @@
96
118
  if (dcApiUrl) {
97
119
  loadInitial(dcApiUrl, sourceConnectionId);
98
120
  } else {
121
+ fetchAbort?.abort();
99
122
  entries = [];
123
+ totalCount = 0;
100
124
  error = '';
101
- tooMany = false;
102
125
  }
103
126
  });
104
127
 
105
- /**
106
- * Initial load fetch MAX_ENTRIES + 1 to detect overflow.
107
- * If ≤ MAX_ENTRIES items, stay in client mode.
108
- * If more, switch to server search mode.
109
- */
110
- async function loadInitial(apiUrl: string, connId?: string) {
111
- // Cancel any in-flight search
112
- searchAbort?.abort();
113
- searchAbort = null;
128
+ /** (Re)load page 1 for the match-all query. Never leaves the table blank on success. */
129
+ async function loadInitial(apiUrl: string, connId: string = '') {
130
+ fetchAbort?.abort();
131
+ const abort = new AbortController();
132
+ fetchAbort = abort;
114
133
 
115
134
  loading = true;
116
135
  error = '';
117
- tooMany = false;
136
+ loadMoreError = '';
137
+ currentQuery = MATCH_ALL;
138
+ offset = 0;
118
139
  entries = [];
140
+ totalCount = 0;
119
141
 
120
142
  try {
121
143
  const dc = getClient(apiUrl);
122
- const result = await dc.catalogEntries.search('%', {
144
+ const result = await dc.catalogEntries.search(MATCH_ALL, {
123
145
  sourceConnectionIds: connId ? [connId] : [],
124
- limit: MAX_ENTRIES + 1
125
- });
126
- const items = result.items ?? [];
127
- const uniqueItems = deduplicateEntries(items);
128
-
129
- if (uniqueItems.length > MAX_ENTRIES) {
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.
133
- tooMany = true;
134
- entries = uniqueItems.slice(0, MAX_ENTRIES);
135
- } else {
136
- tooMany = false;
137
- entries = uniqueItems;
138
- }
146
+ limit: PAGE_SIZE,
147
+ offset: 0
148
+ }, abort.signal);
149
+ if (abort.signal.aborted) return;
150
+ const items = deduplicateEntries(result.items ?? []);
151
+ entries = items;
152
+ totalCount = estimateTotal(result.totalCount, items.length, items.length);
153
+ offset = items.length;
154
+ upsertSelectedCache(selectedCache, items);
139
155
  } catch (err: any) {
140
- error = err.message ?? 'Failed to load catalog entries';
156
+ if (abort.signal.aborted) return;
157
+ error = err?.message ?? 'Failed to load catalog entries';
141
158
  entries = [];
142
159
  } finally {
143
- loading = false;
160
+ if (!abort.signal.aborted) loading = false;
144
161
  }
145
162
  }
146
163
 
147
- /**
148
- * Server-side search — called by TagBrowser when in server mode (tooMany).
149
- * Cancels previous in-flight request via AbortSignal.
150
- */
151
- function handleServerSearch(query: string) {
164
+ /** Server-side search (large catalogs). Resets pagination for the new query. */
165
+ function handleServerSearch(rawQuery: string) {
152
166
  if (!dcApiUrl) return;
153
-
154
- // Cancel previous in-flight search
155
- searchAbort?.abort();
156
-
157
- if (!query) {
158
- // Empty query in too-many mode → restore the first-N preview.
159
- searching = false;
160
- loadInitial(dcApiUrl, sourceConnectionId);
161
- return;
162
- }
163
-
167
+ fetchAbort?.abort();
164
168
  const abort = new AbortController();
165
- searchAbort = abort;
169
+ fetchAbort = abort;
170
+
171
+ const query = normalizeQuery(rawQuery);
172
+ currentQuery = query;
173
+ offset = 0;
174
+ entries = [];
175
+ totalCount = 0;
176
+ error = '';
177
+ loadMoreError = '';
166
178
  searching = true;
167
179
 
168
180
  const dc = getClient(dcApiUrl);
169
181
  dc.catalogEntries.search(query, {
170
182
  sourceConnectionIds: sourceConnectionId ? [sourceConnectionId] : [],
171
- limit: MAX_ENTRIES
183
+ limit: PAGE_SIZE,
184
+ offset: 0
172
185
  }, abort.signal)
173
186
  .then((result) => {
174
187
  if (abort.signal.aborted) return;
175
- const items = result.items ?? [];
176
- entries = deduplicateEntries(items);
188
+ const items = deduplicateEntries(result.items ?? []);
189
+ entries = items;
190
+ totalCount = estimateTotal(result.totalCount, items.length, items.length);
191
+ offset = items.length;
192
+ upsertSelectedCache(selectedCache, items);
177
193
  searching = false;
178
194
  })
179
- .catch((err) => {
195
+ .catch((err: any) => {
180
196
  if (abort.signal.aborted) return;
181
- error = err.message ?? 'Search failed';
197
+ error = err?.message ?? 'Search failed';
182
198
  entries = [];
183
199
  searching = false;
184
200
  });
185
201
  }
186
202
 
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[]>([]);
203
+ /** Append the next page for the current query. No-op when everything is loaded. */
204
+ function handleLoadMore() {
205
+ if (!dcApiUrl || loadingMore || loading || searching) return;
206
+ if (!hasMore(entries.length, totalCount)) return;
195
207
 
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));
208
+ fetchAbort?.abort();
209
+ const abort = new AbortController();
210
+ fetchAbort = abort;
211
211
 
212
- if (missing.length === 0) {
213
- if (keep.length !== current.length) selectedEntries = keep;
214
- return;
215
- }
212
+ loadingMore = true;
213
+ loadMoreError = '';
214
+ const startOffset = offset;
216
215
 
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));
216
+ const dc = getClient(dcApiUrl);
217
+ dc.catalogEntries.search(currentQuery, {
218
+ sourceConnectionIds: sourceConnectionId ? [sourceConnectionId] : [],
219
+ limit: PAGE_SIZE,
220
+ offset: startOffset
221
+ }, abort.signal)
222
+ .then((result) => {
223
+ if (abort.signal.aborted) return;
224
+ const items = result.items ?? [];
225
+ entries = mergePage(entries, items);
226
+ totalCount = estimateTotal(result.totalCount, entries.length, items.length);
227
+ offset = entries.length;
228
+ upsertSelectedCache(selectedCache, items);
229
+ loadingMore = false;
223
230
  })
224
- .catch(() => {
225
- // Keep what we have; SelectedTags falls back to the browse list below.
231
+ .catch((err: any) => {
232
+ if (abort.signal.aborted) return;
233
+ loadMoreError = err?.message ?? 'Failed to load more entries';
234
+ loadingMore = false;
226
235
  });
227
- });
236
+ }
237
+
238
+ const remaining = $derived(remainingCount(entries.length, totalCount));
228
239
  </script>
229
240
 
230
241
  <TagBrowser
@@ -232,17 +243,22 @@
232
243
  bind:selectedIds
233
244
  {columns}
234
245
  {loading}
246
+ {loadingMore}
235
247
  {error}
248
+ {loadMoreError}
236
249
  {emptyMessage}
237
- {onSelectionChange}
238
- onSearch={tooMany ? handleServerSearch : null}
250
+ {totalCount}
251
+ {remaining}
252
+ serverSearch={serverSearch}
239
253
  {searching}
240
- {tooMany}
254
+ {onSelectionChange}
255
+ onSearch={handleServerSearch}
256
+ onLoadMore={handleLoadMore}
241
257
  />
242
258
 
243
259
  {#if selectedIds.length > 0}
244
260
  <SelectedTags
245
- entries={selectedEntries.length > 0 ? selectedEntries : entries}
261
+ entries={selectedEntries}
246
262
  bind:selectedIds
247
263
  columns={selectedColumnsDisplay}
248
264
  {onRemove}
@@ -1,4 +1,5 @@
1
1
  <script>
2
+ import { onDestroy } from 'svelte';
2
3
  import { DEFAULT_COLUMNS, resolveColumn, resolveValue, resolveSearchText } from './picker-column-helpers';
3
4
 
4
5
  let {
@@ -6,18 +7,23 @@
6
7
  selectedIds = $bindable([]),
7
8
  columns = DEFAULT_COLUMNS,
8
9
  loading = false,
10
+ loadingMore = false,
9
11
  error = '',
12
+ loadMoreError = '',
10
13
  emptyMessage = 'No entries found for this source connection.',
14
+ totalCount = 0,
15
+ remaining = 0,
16
+ serverSearch = false,
17
+ searching = false,
11
18
  onSelectionChange = null,
12
19
  onSearch = null,
13
- searching = false,
14
- tooMany = false
20
+ onLoadMore = null
15
21
  } = $props();
16
22
 
17
23
  let searchQuery = $state('');
18
24
  let debounceTimer = null;
19
25
 
20
- /** Resolved column descriptors for rendering — maps each column key to its label, resolver and render type */
26
+ /** Resolved column descriptors for rendering. */
21
27
  const columnDescriptors = $derived(
22
28
  columns
23
29
  .map(k => { const col = resolveColumn(k); return col ? { key: k, ...col } : null; })
@@ -25,31 +31,40 @@
25
31
  );
26
32
 
27
33
  /**
28
- * When onSearch is set (server mode), entries are already filtered server-side.
29
- * When onSearch is NOT set (client mode), filter locally.
34
+ * In server-search mode entries are already filtered server-side.
35
+ * Otherwise (whole catalog loaded) filter the in-memory entries locally.
30
36
  */
31
37
  const filtered = $derived(() => {
32
- if (onSearch) return entries;
38
+ if (serverSearch) return entries;
33
39
  if (!searchQuery.trim()) return entries;
34
40
  const q = searchQuery.toLowerCase();
35
41
  return entries.filter(e => resolveSearchText(e, columns).includes(q));
36
42
  });
37
43
 
38
44
  const selectedCount = $derived(selectedIds.length);
39
- const totalCount = $derived(entries.length);
40
45
  const allVisibleSelected = $derived(filtered().length > 0 && filtered().every(e => selectedIds.includes(e.id)));
41
46
 
47
+ /** Whether the load-more affordance should show (more pages for the current query). */
48
+ const showLoadMore = $derived(remaining > 0 && !loading && !error);
49
+
42
50
  function handleSearchInput(e) {
43
51
  searchQuery = e.target.value;
44
- if (!onSearch) return;
45
52
 
46
- // Debounce 1s for server search
53
+ // Always cancel a pending server search: if the mode is (or just became)
54
+ // client-side, no request should fire; if server-side, we re-arm below.
47
55
  if (debounceTimer) clearTimeout(debounceTimer);
56
+ if (!serverSearch) return; // client-side filtering is reactive via filtered()
57
+
58
+ // Debounce 1s for server search
48
59
  debounceTimer = setTimeout(() => {
49
- onSearch(searchQuery.trim());
60
+ onSearch?.(searchQuery.trim());
50
61
  }, 1000);
51
62
  }
52
63
 
64
+ onDestroy(() => {
65
+ if (debounceTimer) clearTimeout(debounceTimer);
66
+ });
67
+
53
68
  function toggleEntry(id) {
54
69
  selectedIds = selectedIds.indexOf(id) >= 0
55
70
  ? selectedIds.filter(s => s !== id)
@@ -73,6 +88,21 @@
73
88
  function toggleAll() {
74
89
  allVisibleSelected ? deselectAll() : selectAll();
75
90
  }
91
+
92
+ /**
93
+ * Svelte action: fires `onLoadMore` when the sentinel scrolls into the
94
+ * viewport of the scrollable table container. The "Load more" button is
95
+ * the accessible/explicit fallback for the same callback.
96
+ */
97
+ function autoLoad(node) {
98
+ const observer = new IntersectionObserver((entriesObserved) => {
99
+ if (entriesObserved.some(en => en.isIntersecting) && !loadingMore) {
100
+ onLoadMore?.();
101
+ }
102
+ }, { root: node.closest('.table-container'), rootMargin: '120px' });
103
+ observer.observe(node);
104
+ return { destroy() { observer.disconnect(); } };
105
+ }
76
106
  </script>
77
107
 
78
108
  <div class="tag-browser">
@@ -84,7 +114,7 @@
84
114
  <input
85
115
  type="text"
86
116
  class="search-input"
87
- placeholder={tooMany ? "Search to find entries..." : "Search..."}
117
+ placeholder={serverSearch ? "Search to find entries..." : "Search..."}
88
118
  value={searchQuery}
89
119
  oninput={handleSearchInput}
90
120
  />
@@ -107,14 +137,8 @@
107
137
  <span>{error}</span>
108
138
  </div>
109
139
  {:else if entries.length === 0 && !searching}
110
- <div class="state-message empty-state"><span>{tooMany ? 'No matching entries. Refine your search.' : emptyMessage}</span></div>
140
+ <div class="state-message empty-state"><span>{emptyMessage}</span></div>
111
141
  {: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}
118
142
  <div class="table-container">
119
143
  <table class="entry-table">
120
144
  <!-- Table headers: one checkbox column + one column per columnDescriptor -->
@@ -156,8 +180,22 @@
156
180
  {/each}
157
181
  </tbody>
158
182
  </table>
183
+ {#if showLoadMore}
184
+ <div class="load-more" use:autoLoad>
185
+ {#if loadingMore}
186
+ <div class="spinner"></div> <span>Loading more…</span>
187
+ {:else}
188
+ <button class="action-button" onclick={() => onLoadMore?.()}>
189
+ Load more ({remaining} remaining)
190
+ </button>
191
+ {/if}
192
+ </div>
193
+ {#if loadMoreError}
194
+ <div class="load-more-error">{loadMoreError} — <button class="link-button" onclick={() => onLoadMore?.()}>retry</button></div>
195
+ {/if}
196
+ {/if}
159
197
  </div>
160
- {#if !onSearch && filtered().length !== entries.length}
198
+ {#if !serverSearch && filtered().length !== entries.length}
161
199
  <div class="filter-info">Showing {filtered().length} of {entries.length} entries</div>
162
200
  {/if}
163
201
  {/if}
@@ -201,14 +239,6 @@
201
239
  }
202
240
  .error-state { color: var(--cds-support-error, #fa4d56); }
203
241
  .empty-state { color: var(--cds-text-helper, #8d8d8d); }
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; }
212
242
  .spinner {
213
243
  width: 1rem; height: 1rem;
214
244
  border: 2px solid var(--cds-border-subtle-01, #e0e0e0);
@@ -260,4 +290,18 @@
260
290
  font-size: 0.75rem; color: var(--cds-text-helper, #8d8d8d);
261
291
  text-align: center; padding: 0.25rem;
262
292
  }
293
+ .load-more {
294
+ display: flex; align-items: center; justify-content: center;
295
+ gap: 0.5rem; padding: 0.75rem; font-size: 0.8125rem;
296
+ color: var(--cds-text-secondary, #c6c6c6);
297
+ }
298
+ .load-more-error {
299
+ font-size: 0.75rem; color: var(--cds-support-error, #fa4d56);
300
+ text-align: center; padding: 0.25rem;
301
+ }
302
+ .link-button {
303
+ background: none; border: none; padding: 0; cursor: pointer;
304
+ color: var(--cds-link-primary, #78a9ff); text-decoration: underline;
305
+ font-size: inherit;
306
+ }
263
307
  </style>
@@ -8,23 +8,33 @@ declare const TagBrowser: import("svelte").Component<{
8
8
  selectedIds?: any[];
9
9
  columns?: typeof DEFAULT_COLUMNS;
10
10
  loading?: boolean;
11
+ loadingMore?: boolean;
11
12
  error?: string;
13
+ loadMoreError?: string;
12
14
  emptyMessage?: string;
15
+ totalCount?: number;
16
+ remaining?: number;
17
+ serverSearch?: boolean;
18
+ searching?: boolean;
13
19
  onSelectionChange?: any;
14
20
  onSearch?: any;
15
- searching?: boolean;
16
- tooMany?: boolean;
21
+ onLoadMore?: any;
17
22
  }, {}, "selectedIds">;
18
23
  type $$ComponentProps = {
19
24
  entries?: any[];
20
25
  selectedIds?: any[];
21
26
  columns?: typeof DEFAULT_COLUMNS;
22
27
  loading?: boolean;
28
+ loadingMore?: boolean;
23
29
  error?: string;
30
+ loadMoreError?: string;
24
31
  emptyMessage?: string;
32
+ totalCount?: number;
33
+ remaining?: number;
34
+ serverSearch?: boolean;
35
+ searching?: boolean;
25
36
  onSelectionChange?: any;
26
37
  onSearch?: any;
27
- searching?: boolean;
28
- tooMany?: boolean;
38
+ onLoadMore?: any;
29
39
  };
30
40
  import { DEFAULT_COLUMNS } from './picker-column-helpers';
@@ -0,0 +1,24 @@
1
+ import type { CatalogEntry } from '@industream/datacatalog-client/dto';
2
+ /** Number of entries fetched per page. */
3
+ export declare const PAGE_SIZE = 100;
4
+ /** Query token that matches all entries (server-side wildcard). */
5
+ export declare const MATCH_ALL = "%";
6
+ /** Deduplicate entries by id, keeping the first occurrence and preserving order. */
7
+ export declare function deduplicateEntries(items: CatalogEntry[]): CatalogEntry[];
8
+ /** Append a freshly fetched page to the accumulated list, dropping duplicates. */
9
+ export declare function mergePage(existing: CatalogEntry[], page: CatalogEntry[]): CatalogEntry[];
10
+ /** Trim a raw search input; an empty query becomes the match-all token. */
11
+ export declare function normalizeQuery(raw: string): string;
12
+ /** True when the full match-all set is in memory (small catalog → client-side search). */
13
+ export declare function isAllLoaded(query: string, loadedCount: number, totalCount: number): boolean;
14
+ /** Whether more pages remain for the current query. */
15
+ export declare function hasMore(loadedCount: number, totalCount: number): boolean;
16
+ /** Number of not-yet-loaded entries for the current query (never negative). */
17
+ export declare function remainingCount(loadedCount: number, totalCount: number): number;
18
+ /** Upsert every fetched entry into the selected-entry metadata cache (mutates and returns it). */
19
+ export declare function upsertSelectedCache(cache: Map<string, CatalogEntry>, items: CatalogEntry[]): Map<string, CatalogEntry>;
20
+ /**
21
+ * Resolve the selected entries for display: prefer the currently loaded window,
22
+ * fall back to the cache, in the order of `selectedIds`. Ids absent from both are skipped.
23
+ */
24
+ export declare function resolveSelected(entries: CatalogEntry[], cache: Map<string, CatalogEntry>, selectedIds: string[]): CatalogEntry[];