@industream/flowmaker-flowbox-ui-components 0.0.10 → 0.0.12

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,6 +2,11 @@
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
+
5
10
  interface Props {
6
11
  id?: string;
7
12
  dcapiurl?: string;
@@ -9,8 +14,9 @@
9
14
  datatypefilter?: DataType | DataType[] | null;
10
15
  namefilter?: string | string[] | null;
11
16
  initialselection?: string | null;
12
- onentryselect?: (entry: CatalogEntry) => void;
13
- onitemsloaded?: (entries: CatalogEntry[]) => void;
17
+ onentryselect?: (entry: CatalogEntry | null) => void;
18
+ onitemsloaded?: (filtered: CatalogEntry[], all: CatalogEntry[]) => void;
19
+ onsearchmiss?: (text: string) => void; // Called when searchtext matches nothing
14
20
  }
15
21
 
16
22
  let {
@@ -21,7 +27,8 @@
21
27
  namefilter = null,
22
28
  initialselection = null,
23
29
  onentryselect = null,
24
- onitemsloaded = null
30
+ onitemsloaded = null,
31
+ onsearchmiss = null
25
32
  }: Props = $props();
26
33
 
27
34
  let catalogEntries = $state<CatalogEntry[]>([]);
@@ -31,18 +38,22 @@
31
38
  let error = $state<string | null>(null);
32
39
  let dropdownRef = $state<HTMLElement | null>(null);
33
40
 
34
- // Load catalog entries when component mounts or dcapiurl changes
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
+ // Load catalog entries when component mounts or dcapiurl/sourcetypefilter changes
35
49
  $effect(() => {
50
+ // Read sourcetypefilter to establish dependency tracking
51
+ const _sourceTypeFilter = sourcetypefilter;
36
52
  if (dcapiurl) {
37
53
  loadCatalogEntries();
38
54
  }
39
55
  });
40
56
 
41
- // Apply client-side filters when entries or filters change
42
- $effect(() => {
43
- filteredEntries = applyFilters(catalogEntries);
44
- });
45
-
46
57
  // Inject styles into shadow DOM to fix trigger-label width
47
58
  $effect(() => {
48
59
  if (dropdownRef) {
@@ -58,11 +69,10 @@
58
69
  }
59
70
  });
60
71
 
61
- function applyFilters(entries: CatalogEntry[]): CatalogEntry[] {
72
+ function applyBaseFilters(entries: CatalogEntry[]): CatalogEntry[] {
62
73
  let result = entries;
63
74
 
64
75
  // Filter by source type name (from sourceConnection.sourceType.name)
65
- // Note: Server-side filtering is now done via API, this is just for additional client-side filtering if needed
66
76
  if (sourcetypefilter && sourcetypefilter.length > 0) {
67
77
  const sourceTypeNames = Array.isArray(sourcetypefilter) ? sourcetypefilter : [sourcetypefilter];
68
78
  result = result.filter(entry =>
@@ -82,6 +92,54 @@
82
92
  return result;
83
93
  }
84
94
 
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
+
85
143
  async function loadCatalogEntries() {
86
144
  loading = true;
87
145
  error = null;
@@ -102,9 +160,13 @@
102
160
 
103
161
  const result = await client.catalogEntries.get(filters);
104
162
  catalogEntries = result.items || [];
105
- // Apply filters synchronously so initialselection works
106
- filteredEntries = applyFilters(catalogEntries);
107
- console.log('Loaded catalog entries:', catalogEntries);
163
+ // Apply base filters synchronously so initialselection works
164
+ filteredEntries = applyBaseFilters(catalogEntries);
165
+
166
+ // Re-apply dynamic filters if any are active
167
+ if (activeSearchtext || activeDatasetfilter) {
168
+ runFilters();
169
+ }
108
170
 
109
171
  // Auto-select initial selection if provided
110
172
  if (initialselection) {
@@ -115,7 +177,7 @@
115
177
  }
116
178
  }
117
179
 
118
- onitemsloaded?.(filteredEntries);
180
+ onitemsloaded?.(filteredEntries, catalogEntries);
119
181
  } catch (e) {
120
182
  console.error('Failed to load catalog entries:', e);
121
183
  error = e.message || 'Failed to load catalog entries';
@@ -127,33 +189,39 @@
127
189
 
128
190
  function handleSelect(e: CustomEvent) {
129
191
  const selectedId = e.detail?.item?.value || e.target?.value;
130
- console.log('Dropdown selected event:', e, 'selectedId:', selectedId);
131
192
  selectedValue = selectedId;
132
193
 
133
194
  if (selectedId && onentryselect) {
134
195
  const selected = filteredEntries.find(ce => ce.id === selectedId);
135
- console.log('Found selected:', selected);
136
196
  if (selected) {
137
197
  onentryselect(selected);
138
198
  }
139
199
  }
200
+ }
140
201
 
202
+ // Set dynamic filters and re-run filter logic. Call with {} to clear all dynamic filters.
203
+ export function setFilters(opts: FilterOptions) {
204
+ activeSearchtext = opts.searchtext ?? null;
205
+ activeDatasetfilter = opts.datasetfilter ?? null;
206
+ runFilters();
141
207
  }
142
208
 
143
- // Exposed method to programmatically select a catalog entry
144
- export function select(idOrEntry: string | CatalogEntry) {
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
+ }
145
217
  const id = typeof idOrEntry === 'string' ? idOrEntry : idOrEntry?.id;
146
218
  if (!id) return;
147
219
 
148
220
  const entry = filteredEntries.find(ce => ce.id === id);
149
221
  if (entry) {
150
222
  selectedValue = id;
151
- if (dropdownRef) {
152
- dropdownRef.value = id;
153
- }
154
- if (onentryselect) {
155
- onentryselect(entry);
156
- }
223
+ if (dropdownRef) dropdownRef.value = id;
224
+ onentryselect?.(entry);
157
225
  }
158
226
  }
159
227
 
@@ -171,6 +239,13 @@
171
239
  export function getAllEntries(): CatalogEntry[] {
172
240
  return catalogEntries;
173
241
  }
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
+ }
174
249
  </script>
175
250
 
176
251
  {#if loading}
@@ -210,8 +285,42 @@
210
285
  </cds-dropdown-item>
211
286
  {/each}
212
287
  </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}
213
304
  {/if}
214
305
 
215
306
  <style>
216
- /* Styles moved to inline for shadow DOM compatibility */
307
+ .search-miss {
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
+ }
217
326
  </style>
@@ -1,4 +1,8 @@
1
1
  import type { CatalogEntry, DataType } from '@industream/datacatalog-client/dto';
2
+ interface FilterOptions {
3
+ searchtext?: string | null;
4
+ datasetfilter?: string | null;
5
+ }
2
6
  interface Props {
3
7
  id?: string;
4
8
  dcapiurl?: string;
@@ -6,14 +10,17 @@ interface Props {
6
10
  datatypefilter?: DataType | DataType[] | null;
7
11
  namefilter?: string | string[] | null;
8
12
  initialselection?: string | null;
9
- onentryselect?: (entry: CatalogEntry) => void;
10
- onitemsloaded?: (entries: CatalogEntry[]) => void;
13
+ onentryselect?: (entry: CatalogEntry | null) => void;
14
+ onitemsloaded?: (filtered: CatalogEntry[], all: CatalogEntry[]) => void;
15
+ onsearchmiss?: (text: string) => void;
11
16
  }
12
17
  declare const DCCatalogEntry: import("svelte").Component<Props, {
13
- select: (idOrEntry: string | CatalogEntry) => void;
18
+ setFilters: (opts: FilterOptions) => void;
19
+ select: (idOrEntry: string | CatalogEntry | null) => void;
14
20
  getSelection: () => CatalogEntry | null;
15
21
  getEntries: () => CatalogEntry[];
16
22
  getAllEntries: () => CatalogEntry[];
23
+ reload: (selectAfterReload?: string) => void;
17
24
  }, "">;
18
25
  type DCCatalogEntry = ReturnType<typeof DCCatalogEntry>;
19
26
  export default DCCatalogEntry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@industream/flowmaker-flowbox-ui-components",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Reusable Svelte components for FlowMaker FlowBox UI",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",
@@ -2,6 +2,11 @@
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
+
5
10
  interface Props {
6
11
  id?: string;
7
12
  dcapiurl?: string;
@@ -9,8 +14,9 @@
9
14
  datatypefilter?: DataType | DataType[] | null;
10
15
  namefilter?: string | string[] | null;
11
16
  initialselection?: string | null;
12
- onentryselect?: (entry: CatalogEntry) => void;
13
- onitemsloaded?: (entries: CatalogEntry[]) => void;
17
+ onentryselect?: (entry: CatalogEntry | null) => void;
18
+ onitemsloaded?: (filtered: CatalogEntry[], all: CatalogEntry[]) => void;
19
+ onsearchmiss?: (text: string) => void; // Called when searchtext matches nothing
14
20
  }
15
21
 
16
22
  let {
@@ -21,7 +27,8 @@
21
27
  namefilter = null,
22
28
  initialselection = null,
23
29
  onentryselect = null,
24
- onitemsloaded = null
30
+ onitemsloaded = null,
31
+ onsearchmiss = null
25
32
  }: Props = $props();
26
33
 
27
34
  let catalogEntries = $state<CatalogEntry[]>([]);
@@ -31,18 +38,22 @@
31
38
  let error = $state<string | null>(null);
32
39
  let dropdownRef = $state<HTMLElement | null>(null);
33
40
 
34
- // Load catalog entries when component mounts or dcapiurl changes
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
+ // Load catalog entries when component mounts or dcapiurl/sourcetypefilter changes
35
49
  $effect(() => {
50
+ // Read sourcetypefilter to establish dependency tracking
51
+ const _sourceTypeFilter = sourcetypefilter;
36
52
  if (dcapiurl) {
37
53
  loadCatalogEntries();
38
54
  }
39
55
  });
40
56
 
41
- // Apply client-side filters when entries or filters change
42
- $effect(() => {
43
- filteredEntries = applyFilters(catalogEntries);
44
- });
45
-
46
57
  // Inject styles into shadow DOM to fix trigger-label width
47
58
  $effect(() => {
48
59
  if (dropdownRef) {
@@ -58,11 +69,10 @@
58
69
  }
59
70
  });
60
71
 
61
- function applyFilters(entries: CatalogEntry[]): CatalogEntry[] {
72
+ function applyBaseFilters(entries: CatalogEntry[]): CatalogEntry[] {
62
73
  let result = entries;
63
74
 
64
75
  // Filter by source type name (from sourceConnection.sourceType.name)
65
- // Note: Server-side filtering is now done via API, this is just for additional client-side filtering if needed
66
76
  if (sourcetypefilter && sourcetypefilter.length > 0) {
67
77
  const sourceTypeNames = Array.isArray(sourcetypefilter) ? sourcetypefilter : [sourcetypefilter];
68
78
  result = result.filter(entry =>
@@ -82,6 +92,54 @@
82
92
  return result;
83
93
  }
84
94
 
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
+
85
143
  async function loadCatalogEntries() {
86
144
  loading = true;
87
145
  error = null;
@@ -102,9 +160,13 @@
102
160
 
103
161
  const result = await client.catalogEntries.get(filters);
104
162
  catalogEntries = result.items || [];
105
- // Apply filters synchronously so initialselection works
106
- filteredEntries = applyFilters(catalogEntries);
107
- console.log('Loaded catalog entries:', catalogEntries);
163
+ // Apply base filters synchronously so initialselection works
164
+ filteredEntries = applyBaseFilters(catalogEntries);
165
+
166
+ // Re-apply dynamic filters if any are active
167
+ if (activeSearchtext || activeDatasetfilter) {
168
+ runFilters();
169
+ }
108
170
 
109
171
  // Auto-select initial selection if provided
110
172
  if (initialselection) {
@@ -115,7 +177,7 @@
115
177
  }
116
178
  }
117
179
 
118
- onitemsloaded?.(filteredEntries);
180
+ onitemsloaded?.(filteredEntries, catalogEntries);
119
181
  } catch (e) {
120
182
  console.error('Failed to load catalog entries:', e);
121
183
  error = e.message || 'Failed to load catalog entries';
@@ -127,33 +189,39 @@
127
189
 
128
190
  function handleSelect(e: CustomEvent) {
129
191
  const selectedId = e.detail?.item?.value || e.target?.value;
130
- console.log('Dropdown selected event:', e, 'selectedId:', selectedId);
131
192
  selectedValue = selectedId;
132
193
 
133
194
  if (selectedId && onentryselect) {
134
195
  const selected = filteredEntries.find(ce => ce.id === selectedId);
135
- console.log('Found selected:', selected);
136
196
  if (selected) {
137
197
  onentryselect(selected);
138
198
  }
139
199
  }
200
+ }
140
201
 
202
+ // Set dynamic filters and re-run filter logic. Call with {} to clear all dynamic filters.
203
+ export function setFilters(opts: FilterOptions) {
204
+ activeSearchtext = opts.searchtext ?? null;
205
+ activeDatasetfilter = opts.datasetfilter ?? null;
206
+ runFilters();
141
207
  }
142
208
 
143
- // Exposed method to programmatically select a catalog entry
144
- export function select(idOrEntry: string | CatalogEntry) {
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
+ }
145
217
  const id = typeof idOrEntry === 'string' ? idOrEntry : idOrEntry?.id;
146
218
  if (!id) return;
147
219
 
148
220
  const entry = filteredEntries.find(ce => ce.id === id);
149
221
  if (entry) {
150
222
  selectedValue = id;
151
- if (dropdownRef) {
152
- dropdownRef.value = id;
153
- }
154
- if (onentryselect) {
155
- onentryselect(entry);
156
- }
223
+ if (dropdownRef) dropdownRef.value = id;
224
+ onentryselect?.(entry);
157
225
  }
158
226
  }
159
227
 
@@ -171,6 +239,13 @@
171
239
  export function getAllEntries(): CatalogEntry[] {
172
240
  return catalogEntries;
173
241
  }
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
+ }
174
249
  </script>
175
250
 
176
251
  {#if loading}
@@ -210,8 +285,42 @@
210
285
  </cds-dropdown-item>
211
286
  {/each}
212
287
  </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}
213
304
  {/if}
214
305
 
215
306
  <style>
216
- /* Styles moved to inline for shadow DOM compatibility */
307
+ .search-miss {
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
+ }
217
326
  </style>