@syndew/oluso-admin-ui 0.0.0-stage → 0.0.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.
Files changed (56) hide show
  1. package/README.md +6 -2
  2. package/fesm2022/syndew-oluso-admin-ui-entitlements-page.component-BeZtIUnk.mjs +149 -0
  3. package/fesm2022/syndew-oluso-admin-ui-entitlements-page.component-BeZtIUnk.mjs.map +1 -0
  4. package/fesm2022/syndew-oluso-admin-ui-license-status-page.component-JQmj_qhz.mjs +205 -0
  5. package/fesm2022/syndew-oluso-admin-ui-license-status-page.component-JQmj_qhz.mjs.map +1 -0
  6. package/fesm2022/syndew-oluso-admin-ui-platform-clients-page.component-ab3WDRsI.mjs +80 -0
  7. package/fesm2022/syndew-oluso-admin-ui-platform-clients-page.component-ab3WDRsI.mjs.map +1 -0
  8. package/fesm2022/syndew-oluso-admin-ui-search-results-page.component-EH82yuW3.mjs +543 -0
  9. package/fesm2022/syndew-oluso-admin-ui-search-results-page.component-EH82yuW3.mjs.map +1 -0
  10. package/fesm2022/syndew-oluso-admin-ui-syndew-oluso-admin-ui-BVs_bAxm.mjs +22987 -0
  11. package/fesm2022/syndew-oluso-admin-ui-syndew-oluso-admin-ui-BVs_bAxm.mjs.map +1 -0
  12. package/fesm2022/syndew-oluso-admin-ui-team-detail-page.component-CjrQ89qU.mjs +566 -0
  13. package/fesm2022/syndew-oluso-admin-ui-team-detail-page.component-CjrQ89qU.mjs.map +1 -0
  14. package/fesm2022/syndew-oluso-admin-ui-unauthorized-page.component-CWYFN6mp.mjs +73 -0
  15. package/fesm2022/syndew-oluso-admin-ui-unauthorized-page.component-CWYFN6mp.mjs.map +1 -0
  16. package/fesm2022/syndew-oluso-admin-ui-webhook-detail-page.component-Bh5dZwt-.mjs +728 -0
  17. package/fesm2022/syndew-oluso-admin-ui-webhook-detail-page.component-Bh5dZwt-.mjs.map +1 -0
  18. package/fesm2022/syndew-oluso-admin-ui.mjs +3 -0
  19. package/fesm2022/syndew-oluso-admin-ui.mjs.map +1 -0
  20. package/help/en/admin.api-scopes.detail.md +43 -0
  21. package/help/en/admin.api-scopes.md +37 -0
  22. package/help/en/admin.audit-logs.md +44 -0
  23. package/help/en/admin.clients.create.md +60 -0
  24. package/help/en/admin.clients.detail.md +61 -0
  25. package/help/en/admin.clients.md +48 -0
  26. package/help/en/admin.dashboard.md +28 -0
  27. package/help/en/admin.experience.journeys.builder.md +115 -0
  28. package/help/en/admin.experience.journeys.md +54 -0
  29. package/help/en/admin.experience.plugins.md +83 -0
  30. package/help/en/admin.experience.submissions.md +71 -0
  31. package/help/en/admin.feedback.detail.md +36 -0
  32. package/help/en/admin.feedback.md +42 -0
  33. package/help/en/admin.grants.md +42 -0
  34. package/help/en/admin.identity-providers.detail.md +46 -0
  35. package/help/en/admin.identity-providers.md +39 -0
  36. package/help/en/admin.identity-resources.md +47 -0
  37. package/help/en/admin.module-seats.md +45 -0
  38. package/help/en/admin.organizations.detail.md +51 -0
  39. package/help/en/admin.organizations.md +38 -0
  40. package/help/en/admin.platform.clients.create.md +34 -0
  41. package/help/en/admin.platform.clients.detail.md +28 -0
  42. package/help/en/admin.platform.clients.md +38 -0
  43. package/help/en/admin.resources.detail.md +44 -0
  44. package/help/en/admin.resources.md +43 -0
  45. package/help/en/admin.roles.md +33 -0
  46. package/help/en/admin.settings.license.md +39 -0
  47. package/help/en/admin.settings.md +70 -0
  48. package/help/en/admin.signing-keys.md +47 -0
  49. package/help/en/admin.tenants.detail.md +58 -0
  50. package/help/en/admin.tenants.md +46 -0
  51. package/help/en/admin.users.detail.md +41 -0
  52. package/help/en/admin.users.md +56 -0
  53. package/help/en/admin.webhooks.detail.md +41 -0
  54. package/help/en/admin.webhooks.md +37 -0
  55. package/package.json +32 -3
  56. package/types/syndew-oluso-admin-ui.d.ts +5786 -0
@@ -0,0 +1,543 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, signal, computed, Component } from '@angular/core';
3
+ import { CommonModule } from '@angular/common';
4
+ import * as i1 from '@angular/forms';
5
+ import { FormsModule } from '@angular/forms';
6
+ import { ActivatedRoute, Router } from '@angular/router';
7
+ import { Subject, takeUntil, switchMap, of, catchError } from 'rxjs';
8
+ import { IconComponent } from '@syndew/oluso-ui-core';
9
+ import { A as AdminSearchService, T as TenantService, M as MIN_SEARCH_LENGTH, P as PLATFORM_MODULE, g as groupProvidersByModule, a as MODULE_LABEL_FALLBACKS, m as moduleTone, b as badgeTone, w as workspaceHref, s as searchPageParams } from './syndew-oluso-admin-ui-syndew-oluso-admin-ui-BVs_bAxm.mjs';
10
+
11
+ /**
12
+ * Full results for a query: every record kind the caller may see, or one kind
13
+ * with its filters when narrowed. State lives in the URL so a result set can be
14
+ * refreshed or shared.
15
+ */
16
+ class SearchResultsPageComponent {
17
+ searchService = inject(AdminSearchService);
18
+ tenantService = inject(TenantService);
19
+ route = inject(ActivatedRoute);
20
+ router = inject(Router);
21
+ destroy$ = new Subject();
22
+ minLength = MIN_SEARCH_LENGTH;
23
+ platformModule = PLATFORM_MODULE;
24
+ draft = '';
25
+ query = signal('', ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
26
+ providerId = signal(null, ...(ngDevMode ? [{ debugName: "providerId" }] : /* istanbul ignore next */ []));
27
+ filters = signal({}, ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
28
+ providers = signal([], ...(ngDevMode ? [{ debugName: "providers" }] : /* istanbul ignore next */ []));
29
+ modules = signal({}, ...(ngDevMode ? [{ debugName: "modules" }] : /* istanbul ignore next */ []));
30
+ response = signal(null, ...(ngDevMode ? [{ debugName: "response" }] : /* istanbul ignore next */ []));
31
+ loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
32
+ selectedProvider = computed(() => this.providers().find(p => p.providerId === this.providerId()) ?? null, ...(ngDevMode ? [{ debugName: "selectedProvider" }] : /* istanbul ignore next */ []));
33
+ hasFilters = computed(() => Object.values(this.filters()).some(v => v !== ''), ...(ngDevMode ? [{ debugName: "hasFilters" }] : /* istanbul ignore next */ []));
34
+ providerGroups = computed(() => groupProvidersByModule(this.providers(), m => this.moduleLabel(m)), ...(ngDevMode ? [{ debugName: "providerGroups" }] : /* istanbul ignore next */ []));
35
+ placeholder = computed(() => {
36
+ const provider = this.selectedProvider();
37
+ if (!provider)
38
+ return 'Search everything you can see';
39
+ const scope = provider.module === PLATFORM_MODULE ? '' : `${this.moduleLabel(provider.module)} `;
40
+ return `Search ${scope}${provider.displayName.toLowerCase()}`;
41
+ }, ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
42
+ ngOnInit() {
43
+ this.searchService.getProviders().pipe(takeUntil(this.destroy$)).subscribe(providers => this.providers.set(providers));
44
+ this.tenantService.getRegisteredModules().pipe(takeUntil(this.destroy$)).subscribe(response => {
45
+ const byKey = {};
46
+ for (const module of response.modules)
47
+ byKey[module.featureKey] = module;
48
+ this.modules.set(byKey);
49
+ });
50
+ this.route.queryParamMap
51
+ .pipe(switchMap(params => {
52
+ const query = (params.get('q') ?? '').trim();
53
+ const providerId = params.get('provider');
54
+ const filters = {};
55
+ for (const key of params.keys) {
56
+ if (key.startsWith('f.'))
57
+ filters[key.slice(2)] = params.get(key) ?? '';
58
+ }
59
+ this.query.set(query);
60
+ this.draft = query;
61
+ this.providerId.set(providerId);
62
+ this.filters.set(filters);
63
+ if (query.length < MIN_SEARCH_LENGTH) {
64
+ this.response.set(null);
65
+ this.loading.set(false);
66
+ return of(null);
67
+ }
68
+ this.loading.set(true);
69
+ return this.searchService.search(query, { providerId: providerId ?? undefined, filters, limit: providerId ? 50 : 10 })
70
+ .pipe(catchError(() => of({ query, totalCount: 0, groups: [] })));
71
+ }), takeUntil(this.destroy$))
72
+ .subscribe(response => {
73
+ this.response.set(response);
74
+ this.loading.set(false);
75
+ });
76
+ }
77
+ ngOnDestroy() {
78
+ this.destroy$.next();
79
+ this.destroy$.complete();
80
+ }
81
+ submit(event) {
82
+ event.preventDefault();
83
+ this.apply(this.draft.trim(), this.providerId(), this.filters());
84
+ }
85
+ /** Emptying the box, by typing or the field's clear button, drops the results straight away. */
86
+ onDraftChange(value) {
87
+ this.draft = value;
88
+ if (value.trim() === '' && this.query() !== '') {
89
+ this.apply('', this.providerId(), this.filters());
90
+ }
91
+ }
92
+ selectProvider(providerId) {
93
+ this.apply(this.query(), providerId, {});
94
+ }
95
+ filterValue(key) {
96
+ return this.filters()[key] ?? '';
97
+ }
98
+ filterValues(key) {
99
+ const value = this.filterValue(key);
100
+ return value ? value.split(',') : [];
101
+ }
102
+ setFilter(key, value) {
103
+ this.apply(this.query(), this.providerId(), { ...this.filters(), [key]: value ?? '' });
104
+ }
105
+ rangePart(key, index) {
106
+ return this.filterValue(key).split('..')[index] ?? '';
107
+ }
108
+ setRange(key, index, value) {
109
+ const parts = this.filterValue(key).split('..');
110
+ parts[index] = value == null ? '' : String(value);
111
+ const joined = `${parts[0] ?? ''}..${parts[1] ?? ''}`;
112
+ this.setFilter(key, joined === '..' ? '' : joined);
113
+ }
114
+ clearFilters() {
115
+ this.apply(this.query(), this.providerId(), {});
116
+ }
117
+ moduleLabel(module) {
118
+ return this.modules()[module]?.displayName ?? MODULE_LABEL_FALLBACKS[module] ?? module;
119
+ }
120
+ moduleTone(module) {
121
+ return moduleTone(this.modules()[module]?.color);
122
+ }
123
+ badgeTone(tone) {
124
+ return badgeTone(tone);
125
+ }
126
+ hrefFor(item) {
127
+ return item.route || workspaceHref(item.workspaceRoute) || '#';
128
+ }
129
+ openItem(event, item) {
130
+ if (item.route) {
131
+ event.preventDefault();
132
+ this.router.navigateByUrl(item.route);
133
+ return;
134
+ }
135
+ if (!workspaceHref(item.workspaceRoute))
136
+ event.preventDefault();
137
+ }
138
+ go(event, route) {
139
+ event.preventDefault();
140
+ this.router.navigateByUrl(route);
141
+ }
142
+ apply(query, providerId, filters) {
143
+ this.router.navigate([], { relativeTo: this.route, queryParams: searchPageParams(query, providerId, filters) });
144
+ }
145
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: SearchResultsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
146
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: SearchResultsPageComponent, isStandalone: true, selector: "admin-search-results-page", ngImport: i0, template: `
147
+ <div class="space-y-6">
148
+ <div class="flex flex-wrap items-end justify-between gap-4">
149
+ <div>
150
+ <h1 class="text-2xl font-bold text-gray-900">Search</h1>
151
+ @if (response(); as result) {
152
+ <p class="mt-1 text-sm text-gray-500">
153
+ {{ result.totalCount }} {{ result.totalCount === 1 ? 'match' : 'matches' }} for “{{ result.query }}”
154
+ </p>
155
+ }
156
+ </div>
157
+ <form class="flex items-center gap-2 w-full max-w-lg" (submit)="submit($event)">
158
+ <div class="relative flex-1">
159
+ <ui-icon name="search" class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
160
+ <input type="search" [ngModel]="draft" (ngModelChange)="onDraftChange($event)" name="q" aria-label="Search" [placeholder]="placeholder()"
161
+ class="w-full pl-9 text-sm border-gray-300 rounded-md shadow-sm focus:border-primary-500 focus:ring-primary-500" />
162
+ </div>
163
+ <button type="submit" class="px-3 py-2 text-sm font-medium text-white bg-primary-600 rounded-md hover:bg-primary-700">Search</button>
164
+ </form>
165
+ </div>
166
+
167
+ <!-- Record kinds, grouped by module -->
168
+ <div class="rounded-lg border border-gray-200 bg-white p-3 space-y-2">
169
+ <div class="flex items-center gap-1.5 flex-wrap">
170
+ <span class="w-24 shrink-0 text-[11px] font-semibold uppercase tracking-wider text-gray-400">Search in</span>
171
+ <button type="button" (click)="selectProvider(null)"
172
+ class="px-2.5 py-1 rounded-full text-xs font-medium border transition-colors"
173
+ [class]="providerId() === null ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-50'">
174
+ Everything
175
+ </button>
176
+ </div>
177
+ @for (moduleGroup of providerGroups(); track moduleGroup.module) {
178
+ <div class="flex items-start gap-1.5 flex-wrap">
179
+ <span class="w-24 shrink-0 pt-1 text-xs font-medium text-gray-500 truncate" [title]="moduleGroup.label">{{ moduleGroup.label }}</span>
180
+ @for (provider of moduleGroup.providers; track provider.providerId) {
181
+ <button type="button" (click)="selectProvider(provider.providerId)"
182
+ class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium border transition-colors"
183
+ [class]="providerId() === provider.providerId ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-50'">
184
+ <ui-icon [name]="provider.icon" class="h-3 w-3" />
185
+ {{ provider.displayName }}
186
+ </button>
187
+ }
188
+ </div>
189
+ }
190
+ </div>
191
+
192
+ <!-- Filters for the selected kind -->
193
+ @if (selectedProvider(); as provider) {
194
+ @if (provider.filters.length > 0) {
195
+ <div class="flex flex-wrap items-end gap-3 p-3 rounded-lg border border-gray-200 bg-gray-50/60">
196
+ @for (filter of provider.filters; track filter.key) {
197
+ <div class="flex flex-col gap-0.5 text-[11px] font-medium text-gray-500">
198
+ <span>{{ filter.label }}</span>
199
+ @switch (filter.type) {
200
+ @case ('select') {
201
+ <select [attr.aria-label]="filter.label" [ngModel]="filterValue(filter.key)" (ngModelChange)="setFilter(filter.key, $event)"
202
+ class="h-8 text-xs border-gray-300 rounded-md py-0 pl-2 pr-7">
203
+ <option value="">Any</option>
204
+ @for (option of filter.options ?? []; track option.value) {
205
+ <option [value]="option.value">{{ option.label }}</option>
206
+ }
207
+ </select>
208
+ }
209
+ @case ('multiselect') {
210
+ <select multiple [attr.aria-label]="filter.label" [ngModel]="filterValues(filter.key)" (ngModelChange)="setFilter(filter.key, $event.join(','))"
211
+ class="text-xs border-gray-300 rounded-md py-0.5 min-w-[8rem]" size="3">
212
+ @for (option of filter.options ?? []; track option.value) {
213
+ <option [value]="option.value">{{ option.label }}</option>
214
+ }
215
+ </select>
216
+ }
217
+ @case ('boolean') {
218
+ <select [attr.aria-label]="filter.label" [ngModel]="filterValue(filter.key)" (ngModelChange)="setFilter(filter.key, $event)"
219
+ class="h-8 text-xs border-gray-300 rounded-md py-0 pl-2 pr-7">
220
+ <option value="">Any</option>
221
+ <option value="true">Yes</option>
222
+ <option value="false">No</option>
223
+ </select>
224
+ }
225
+ @case ('date') {
226
+ <input type="date" [attr.aria-label]="filter.label" [ngModel]="filterValue(filter.key)" (ngModelChange)="setFilter(filter.key, $event)"
227
+ class="h-8 text-xs border-gray-300 rounded-md py-0 px-2" />
228
+ }
229
+ @case ('daterange') {
230
+ <span class="flex items-center gap-1">
231
+ <input type="date" [attr.aria-label]="filter.label + ' from'" [ngModel]="rangePart(filter.key, 0)" (ngModelChange)="setRange(filter.key, 0, $event)"
232
+ class="h-8 text-xs border-gray-300 rounded-md py-0 px-2" />
233
+ <span class="text-gray-400">to</span>
234
+ <input type="date" [attr.aria-label]="filter.label + ' to'" [ngModel]="rangePart(filter.key, 1)" (ngModelChange)="setRange(filter.key, 1, $event)"
235
+ class="h-8 text-xs border-gray-300 rounded-md py-0 px-2" />
236
+ </span>
237
+ }
238
+ @case ('number') {
239
+ <input type="number" [attr.aria-label]="filter.label" [ngModel]="filterValue(filter.key)" (ngModelChange)="setFilter(filter.key, $event)"
240
+ [placeholder]="filter.placeholder ?? ''" class="h-8 w-28 text-xs border-gray-300 rounded-md py-0 px-2" />
241
+ }
242
+ @case ('numberrange') {
243
+ <span class="flex items-center gap-1">
244
+ <input type="number" placeholder="Min" [attr.aria-label]="filter.label + ' minimum'" [ngModel]="rangePart(filter.key, 0)" (ngModelChange)="setRange(filter.key, 0, $event)"
245
+ class="h-8 w-24 text-xs border-gray-300 rounded-md py-0 px-2" />
246
+ <span class="text-gray-400">to</span>
247
+ <input type="number" placeholder="Max" [attr.aria-label]="filter.label + ' maximum'" [ngModel]="rangePart(filter.key, 1)" (ngModelChange)="setRange(filter.key, 1, $event)"
248
+ class="h-8 w-24 text-xs border-gray-300 rounded-md py-0 px-2" />
249
+ </span>
250
+ }
251
+ @default {
252
+ <input type="text" [attr.aria-label]="filter.label" [ngModel]="filterValue(filter.key)" (ngModelChange)="setFilter(filter.key, $event)"
253
+ [placeholder]="filter.placeholder ?? ''" class="h-8 w-40 text-xs border-gray-300 rounded-md py-0 px-2" />
254
+ }
255
+ }
256
+ </div>
257
+ }
258
+ @if (hasFilters()) {
259
+ <button type="button" (click)="clearFilters()" class="h-8 text-xs text-gray-500 hover:text-gray-700 hover:underline">Reset filters</button>
260
+ }
261
+ </div>
262
+ }
263
+ }
264
+
265
+ <!-- Results -->
266
+ @if (query().length < minLength) {
267
+ <div class="rounded-lg border border-dashed border-gray-200 p-10 text-center text-sm text-gray-500">
268
+ Type at least {{ minLength }} characters to search.
269
+ </div>
270
+ } @else if (loading()) {
271
+ <div class="rounded-lg border border-gray-200 p-10 text-center text-sm text-gray-500">Searching…</div>
272
+ } @else if (response(); as result) {
273
+ @if (result.groups.length === 0) {
274
+ <div class="rounded-lg border border-dashed border-gray-200 p-10 text-center text-sm text-gray-500">
275
+ No matches for “{{ result.query }}”.
276
+ </div>
277
+ }
278
+ <div class="grid gap-4" [class.lg:grid-cols-2]="providerId() === null && result.groups.length > 1">
279
+ @for (group of result.groups; track group.providerId) {
280
+ <section class="rounded-lg border border-gray-200 bg-white overflow-hidden">
281
+ <header class="flex items-center justify-between px-4 py-2.5 border-b border-gray-100 bg-gray-50/70">
282
+ <div class="flex items-center gap-2 min-w-0">
283
+ <span class="inline-flex h-6 w-6 items-center justify-center rounded" [class]="moduleTone(group.module)">
284
+ <ui-icon [name]="group.icon" class="h-3.5 w-3.5" />
285
+ </span>
286
+ <h2 class="text-sm font-semibold text-gray-900 truncate">{{ group.displayName }}</h2>
287
+ @if (group.module !== platformModule) {
288
+ <span class="text-xs text-gray-400 truncate">· {{ moduleLabel(group.module) }}</span>
289
+ }
290
+ <span class="text-xs text-gray-400">({{ group.totalCount }})</span>
291
+ </div>
292
+ <div class="flex items-center gap-3 shrink-0 text-xs">
293
+ @if (providerId() === null && group.totalCount > group.items.length) {
294
+ <button type="button" (click)="selectProvider(group.providerId)" class="text-primary-600 hover:text-primary-700">Show all here</button>
295
+ }
296
+ @if (group.seeAllRoute) {
297
+ <a [href]="group.seeAllRoute" (click)="go($event, group.seeAllRoute)" class="text-gray-500 hover:text-gray-700">Open list</a>
298
+ }
299
+ </div>
300
+ </header>
301
+ <ul class="divide-y divide-gray-100">
302
+ @for (item of group.items; track item.id) {
303
+ <li>
304
+ <a [href]="hrefFor(item)" (click)="openItem($event, item)"
305
+ class="flex items-start gap-3 px-4 py-2.5 hover:bg-gray-50 transition-colors">
306
+ @if (item.imageUrl) {
307
+ <img [src]="item.imageUrl" alt="" class="h-9 w-9 rounded-full object-cover shrink-0" />
308
+ } @else {
309
+ <span class="inline-flex h-9 w-9 items-center justify-center rounded-full shrink-0" [class]="moduleTone(group.module)">
310
+ <ui-icon [name]="item.icon ?? group.icon" class="h-4 w-4" />
311
+ </span>
312
+ }
313
+ <span class="min-w-0 flex-1">
314
+ <span class="block truncate text-sm font-medium text-gray-900">{{ item.title }}</span>
315
+ @if (item.subtitle) {
316
+ <span class="block truncate text-xs text-gray-500">{{ item.subtitle }}</span>
317
+ }
318
+ @if (item.description) {
319
+ <span class="block truncate text-xs text-gray-400">{{ item.description }}</span>
320
+ }
321
+ </span>
322
+ @if (item.badges.length > 0) {
323
+ <span class="flex flex-wrap justify-end gap-1 shrink-0 max-w-[14rem]">
324
+ @for (badge of item.badges; track badge.label + badge.value) {
325
+ <span class="inline-flex px-1.5 py-0.5 rounded text-[11px] font-medium" [class]="badgeTone(badge.tone)" [title]="badge.label">{{ badge.value }}</span>
326
+ }
327
+ </span>
328
+ }
329
+ </a>
330
+ </li>
331
+ }
332
+ </ul>
333
+ </section>
334
+ }
335
+ </div>
336
+ }
337
+ </div>
338
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.SelectMultipleControlValueAccessor, selector: "select[multiple][formControlName],select[multiple][formControl],select[multiple][ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: IconComponent, selector: "ui-icon", inputs: ["name", "strokeWidth"] }] });
339
+ }
340
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: SearchResultsPageComponent, decorators: [{
341
+ type: Component,
342
+ args: [{
343
+ selector: 'admin-search-results-page',
344
+ standalone: true,
345
+ imports: [CommonModule, FormsModule, IconComponent],
346
+ template: `
347
+ <div class="space-y-6">
348
+ <div class="flex flex-wrap items-end justify-between gap-4">
349
+ <div>
350
+ <h1 class="text-2xl font-bold text-gray-900">Search</h1>
351
+ @if (response(); as result) {
352
+ <p class="mt-1 text-sm text-gray-500">
353
+ {{ result.totalCount }} {{ result.totalCount === 1 ? 'match' : 'matches' }} for “{{ result.query }}”
354
+ </p>
355
+ }
356
+ </div>
357
+ <form class="flex items-center gap-2 w-full max-w-lg" (submit)="submit($event)">
358
+ <div class="relative flex-1">
359
+ <ui-icon name="search" class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
360
+ <input type="search" [ngModel]="draft" (ngModelChange)="onDraftChange($event)" name="q" aria-label="Search" [placeholder]="placeholder()"
361
+ class="w-full pl-9 text-sm border-gray-300 rounded-md shadow-sm focus:border-primary-500 focus:ring-primary-500" />
362
+ </div>
363
+ <button type="submit" class="px-3 py-2 text-sm font-medium text-white bg-primary-600 rounded-md hover:bg-primary-700">Search</button>
364
+ </form>
365
+ </div>
366
+
367
+ <!-- Record kinds, grouped by module -->
368
+ <div class="rounded-lg border border-gray-200 bg-white p-3 space-y-2">
369
+ <div class="flex items-center gap-1.5 flex-wrap">
370
+ <span class="w-24 shrink-0 text-[11px] font-semibold uppercase tracking-wider text-gray-400">Search in</span>
371
+ <button type="button" (click)="selectProvider(null)"
372
+ class="px-2.5 py-1 rounded-full text-xs font-medium border transition-colors"
373
+ [class]="providerId() === null ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-50'">
374
+ Everything
375
+ </button>
376
+ </div>
377
+ @for (moduleGroup of providerGroups(); track moduleGroup.module) {
378
+ <div class="flex items-start gap-1.5 flex-wrap">
379
+ <span class="w-24 shrink-0 pt-1 text-xs font-medium text-gray-500 truncate" [title]="moduleGroup.label">{{ moduleGroup.label }}</span>
380
+ @for (provider of moduleGroup.providers; track provider.providerId) {
381
+ <button type="button" (click)="selectProvider(provider.providerId)"
382
+ class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium border transition-colors"
383
+ [class]="providerId() === provider.providerId ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-50'">
384
+ <ui-icon [name]="provider.icon" class="h-3 w-3" />
385
+ {{ provider.displayName }}
386
+ </button>
387
+ }
388
+ </div>
389
+ }
390
+ </div>
391
+
392
+ <!-- Filters for the selected kind -->
393
+ @if (selectedProvider(); as provider) {
394
+ @if (provider.filters.length > 0) {
395
+ <div class="flex flex-wrap items-end gap-3 p-3 rounded-lg border border-gray-200 bg-gray-50/60">
396
+ @for (filter of provider.filters; track filter.key) {
397
+ <div class="flex flex-col gap-0.5 text-[11px] font-medium text-gray-500">
398
+ <span>{{ filter.label }}</span>
399
+ @switch (filter.type) {
400
+ @case ('select') {
401
+ <select [attr.aria-label]="filter.label" [ngModel]="filterValue(filter.key)" (ngModelChange)="setFilter(filter.key, $event)"
402
+ class="h-8 text-xs border-gray-300 rounded-md py-0 pl-2 pr-7">
403
+ <option value="">Any</option>
404
+ @for (option of filter.options ?? []; track option.value) {
405
+ <option [value]="option.value">{{ option.label }}</option>
406
+ }
407
+ </select>
408
+ }
409
+ @case ('multiselect') {
410
+ <select multiple [attr.aria-label]="filter.label" [ngModel]="filterValues(filter.key)" (ngModelChange)="setFilter(filter.key, $event.join(','))"
411
+ class="text-xs border-gray-300 rounded-md py-0.5 min-w-[8rem]" size="3">
412
+ @for (option of filter.options ?? []; track option.value) {
413
+ <option [value]="option.value">{{ option.label }}</option>
414
+ }
415
+ </select>
416
+ }
417
+ @case ('boolean') {
418
+ <select [attr.aria-label]="filter.label" [ngModel]="filterValue(filter.key)" (ngModelChange)="setFilter(filter.key, $event)"
419
+ class="h-8 text-xs border-gray-300 rounded-md py-0 pl-2 pr-7">
420
+ <option value="">Any</option>
421
+ <option value="true">Yes</option>
422
+ <option value="false">No</option>
423
+ </select>
424
+ }
425
+ @case ('date') {
426
+ <input type="date" [attr.aria-label]="filter.label" [ngModel]="filterValue(filter.key)" (ngModelChange)="setFilter(filter.key, $event)"
427
+ class="h-8 text-xs border-gray-300 rounded-md py-0 px-2" />
428
+ }
429
+ @case ('daterange') {
430
+ <span class="flex items-center gap-1">
431
+ <input type="date" [attr.aria-label]="filter.label + ' from'" [ngModel]="rangePart(filter.key, 0)" (ngModelChange)="setRange(filter.key, 0, $event)"
432
+ class="h-8 text-xs border-gray-300 rounded-md py-0 px-2" />
433
+ <span class="text-gray-400">to</span>
434
+ <input type="date" [attr.aria-label]="filter.label + ' to'" [ngModel]="rangePart(filter.key, 1)" (ngModelChange)="setRange(filter.key, 1, $event)"
435
+ class="h-8 text-xs border-gray-300 rounded-md py-0 px-2" />
436
+ </span>
437
+ }
438
+ @case ('number') {
439
+ <input type="number" [attr.aria-label]="filter.label" [ngModel]="filterValue(filter.key)" (ngModelChange)="setFilter(filter.key, $event)"
440
+ [placeholder]="filter.placeholder ?? ''" class="h-8 w-28 text-xs border-gray-300 rounded-md py-0 px-2" />
441
+ }
442
+ @case ('numberrange') {
443
+ <span class="flex items-center gap-1">
444
+ <input type="number" placeholder="Min" [attr.aria-label]="filter.label + ' minimum'" [ngModel]="rangePart(filter.key, 0)" (ngModelChange)="setRange(filter.key, 0, $event)"
445
+ class="h-8 w-24 text-xs border-gray-300 rounded-md py-0 px-2" />
446
+ <span class="text-gray-400">to</span>
447
+ <input type="number" placeholder="Max" [attr.aria-label]="filter.label + ' maximum'" [ngModel]="rangePart(filter.key, 1)" (ngModelChange)="setRange(filter.key, 1, $event)"
448
+ class="h-8 w-24 text-xs border-gray-300 rounded-md py-0 px-2" />
449
+ </span>
450
+ }
451
+ @default {
452
+ <input type="text" [attr.aria-label]="filter.label" [ngModel]="filterValue(filter.key)" (ngModelChange)="setFilter(filter.key, $event)"
453
+ [placeholder]="filter.placeholder ?? ''" class="h-8 w-40 text-xs border-gray-300 rounded-md py-0 px-2" />
454
+ }
455
+ }
456
+ </div>
457
+ }
458
+ @if (hasFilters()) {
459
+ <button type="button" (click)="clearFilters()" class="h-8 text-xs text-gray-500 hover:text-gray-700 hover:underline">Reset filters</button>
460
+ }
461
+ </div>
462
+ }
463
+ }
464
+
465
+ <!-- Results -->
466
+ @if (query().length < minLength) {
467
+ <div class="rounded-lg border border-dashed border-gray-200 p-10 text-center text-sm text-gray-500">
468
+ Type at least {{ minLength }} characters to search.
469
+ </div>
470
+ } @else if (loading()) {
471
+ <div class="rounded-lg border border-gray-200 p-10 text-center text-sm text-gray-500">Searching…</div>
472
+ } @else if (response(); as result) {
473
+ @if (result.groups.length === 0) {
474
+ <div class="rounded-lg border border-dashed border-gray-200 p-10 text-center text-sm text-gray-500">
475
+ No matches for “{{ result.query }}”.
476
+ </div>
477
+ }
478
+ <div class="grid gap-4" [class.lg:grid-cols-2]="providerId() === null && result.groups.length > 1">
479
+ @for (group of result.groups; track group.providerId) {
480
+ <section class="rounded-lg border border-gray-200 bg-white overflow-hidden">
481
+ <header class="flex items-center justify-between px-4 py-2.5 border-b border-gray-100 bg-gray-50/70">
482
+ <div class="flex items-center gap-2 min-w-0">
483
+ <span class="inline-flex h-6 w-6 items-center justify-center rounded" [class]="moduleTone(group.module)">
484
+ <ui-icon [name]="group.icon" class="h-3.5 w-3.5" />
485
+ </span>
486
+ <h2 class="text-sm font-semibold text-gray-900 truncate">{{ group.displayName }}</h2>
487
+ @if (group.module !== platformModule) {
488
+ <span class="text-xs text-gray-400 truncate">· {{ moduleLabel(group.module) }}</span>
489
+ }
490
+ <span class="text-xs text-gray-400">({{ group.totalCount }})</span>
491
+ </div>
492
+ <div class="flex items-center gap-3 shrink-0 text-xs">
493
+ @if (providerId() === null && group.totalCount > group.items.length) {
494
+ <button type="button" (click)="selectProvider(group.providerId)" class="text-primary-600 hover:text-primary-700">Show all here</button>
495
+ }
496
+ @if (group.seeAllRoute) {
497
+ <a [href]="group.seeAllRoute" (click)="go($event, group.seeAllRoute)" class="text-gray-500 hover:text-gray-700">Open list</a>
498
+ }
499
+ </div>
500
+ </header>
501
+ <ul class="divide-y divide-gray-100">
502
+ @for (item of group.items; track item.id) {
503
+ <li>
504
+ <a [href]="hrefFor(item)" (click)="openItem($event, item)"
505
+ class="flex items-start gap-3 px-4 py-2.5 hover:bg-gray-50 transition-colors">
506
+ @if (item.imageUrl) {
507
+ <img [src]="item.imageUrl" alt="" class="h-9 w-9 rounded-full object-cover shrink-0" />
508
+ } @else {
509
+ <span class="inline-flex h-9 w-9 items-center justify-center rounded-full shrink-0" [class]="moduleTone(group.module)">
510
+ <ui-icon [name]="item.icon ?? group.icon" class="h-4 w-4" />
511
+ </span>
512
+ }
513
+ <span class="min-w-0 flex-1">
514
+ <span class="block truncate text-sm font-medium text-gray-900">{{ item.title }}</span>
515
+ @if (item.subtitle) {
516
+ <span class="block truncate text-xs text-gray-500">{{ item.subtitle }}</span>
517
+ }
518
+ @if (item.description) {
519
+ <span class="block truncate text-xs text-gray-400">{{ item.description }}</span>
520
+ }
521
+ </span>
522
+ @if (item.badges.length > 0) {
523
+ <span class="flex flex-wrap justify-end gap-1 shrink-0 max-w-[14rem]">
524
+ @for (badge of item.badges; track badge.label + badge.value) {
525
+ <span class="inline-flex px-1.5 py-0.5 rounded text-[11px] font-medium" [class]="badgeTone(badge.tone)" [title]="badge.label">{{ badge.value }}</span>
526
+ }
527
+ </span>
528
+ }
529
+ </a>
530
+ </li>
531
+ }
532
+ </ul>
533
+ </section>
534
+ }
535
+ </div>
536
+ }
537
+ </div>
538
+ `,
539
+ }]
540
+ }] });
541
+
542
+ export { SearchResultsPageComponent };
543
+ //# sourceMappingURL=syndew-oluso-admin-ui-search-results-page.component-EH82yuW3.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syndew-oluso-admin-ui-search-results-page.component-EH82yuW3.mjs","sources":["../../../../libs/admin-ui/src/lib/pages/search/search-results-page.component.ts"],"sourcesContent":["import { Component, OnDestroy, OnInit, computed, inject, signal } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { Subject, catchError, of, switchMap, takeUntil } from 'rxjs';\nimport { IconComponent } from '@syndew/oluso-ui-core';\nimport {\n AdminSearchService, AdminSearchProvider, AdminSearchResultItem, AdminSearchResponse,\n AdminSearchBadgeTone, PLATFORM_MODULE, MIN_SEARCH_LENGTH,\n} from '../../services/admin-search.service';\nimport { TenantService, RegisteredModuleInfo } from '../../services/tenant.service';\nimport { MODULE_LABEL_FALLBACKS, badgeTone, groupProvidersByModule, moduleTone, searchPageParams, workspaceHref } from '../../components/global-search/admin-search-ui';\n\n/**\n * Full results for a query: every record kind the caller may see, or one kind\n * with its filters when narrowed. State lives in the URL so a result set can be\n * refreshed or shared.\n */\n@Component({\n selector: 'admin-search-results-page',\n standalone: true,\n imports: [CommonModule, FormsModule, IconComponent],\n template: `\n <div class=\"space-y-6\">\n <div class=\"flex flex-wrap items-end justify-between gap-4\">\n <div>\n <h1 class=\"text-2xl font-bold text-gray-900\">Search</h1>\n @if (response(); as result) {\n <p class=\"mt-1 text-sm text-gray-500\">\n {{ result.totalCount }} {{ result.totalCount === 1 ? 'match' : 'matches' }} for “{{ result.query }}”\n </p>\n }\n </div>\n <form class=\"flex items-center gap-2 w-full max-w-lg\" (submit)=\"submit($event)\">\n <div class=\"relative flex-1\">\n <ui-icon name=\"search\" class=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none\" />\n <input type=\"search\" [ngModel]=\"draft\" (ngModelChange)=\"onDraftChange($event)\" name=\"q\" aria-label=\"Search\" [placeholder]=\"placeholder()\"\n class=\"w-full pl-9 text-sm border-gray-300 rounded-md shadow-sm focus:border-primary-500 focus:ring-primary-500\" />\n </div>\n <button type=\"submit\" class=\"px-3 py-2 text-sm font-medium text-white bg-primary-600 rounded-md hover:bg-primary-700\">Search</button>\n </form>\n </div>\n\n <!-- Record kinds, grouped by module -->\n <div class=\"rounded-lg border border-gray-200 bg-white p-3 space-y-2\">\n <div class=\"flex items-center gap-1.5 flex-wrap\">\n <span class=\"w-24 shrink-0 text-[11px] font-semibold uppercase tracking-wider text-gray-400\">Search in</span>\n <button type=\"button\" (click)=\"selectProvider(null)\"\n class=\"px-2.5 py-1 rounded-full text-xs font-medium border transition-colors\"\n [class]=\"providerId() === null ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-50'\">\n Everything\n </button>\n </div>\n @for (moduleGroup of providerGroups(); track moduleGroup.module) {\n <div class=\"flex items-start gap-1.5 flex-wrap\">\n <span class=\"w-24 shrink-0 pt-1 text-xs font-medium text-gray-500 truncate\" [title]=\"moduleGroup.label\">{{ moduleGroup.label }}</span>\n @for (provider of moduleGroup.providers; track provider.providerId) {\n <button type=\"button\" (click)=\"selectProvider(provider.providerId)\"\n class=\"inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium border transition-colors\"\n [class]=\"providerId() === provider.providerId ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-50'\">\n <ui-icon [name]=\"provider.icon\" class=\"h-3 w-3\" />\n {{ provider.displayName }}\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Filters for the selected kind -->\n @if (selectedProvider(); as provider) {\n @if (provider.filters.length > 0) {\n <div class=\"flex flex-wrap items-end gap-3 p-3 rounded-lg border border-gray-200 bg-gray-50/60\">\n @for (filter of provider.filters; track filter.key) {\n <div class=\"flex flex-col gap-0.5 text-[11px] font-medium text-gray-500\">\n <span>{{ filter.label }}</span>\n @switch (filter.type) {\n @case ('select') {\n <select [attr.aria-label]=\"filter.label\" [ngModel]=\"filterValue(filter.key)\" (ngModelChange)=\"setFilter(filter.key, $event)\"\n class=\"h-8 text-xs border-gray-300 rounded-md py-0 pl-2 pr-7\">\n <option value=\"\">Any</option>\n @for (option of filter.options ?? []; track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </select>\n }\n @case ('multiselect') {\n <select multiple [attr.aria-label]=\"filter.label\" [ngModel]=\"filterValues(filter.key)\" (ngModelChange)=\"setFilter(filter.key, $event.join(','))\"\n class=\"text-xs border-gray-300 rounded-md py-0.5 min-w-[8rem]\" size=\"3\">\n @for (option of filter.options ?? []; track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </select>\n }\n @case ('boolean') {\n <select [attr.aria-label]=\"filter.label\" [ngModel]=\"filterValue(filter.key)\" (ngModelChange)=\"setFilter(filter.key, $event)\"\n class=\"h-8 text-xs border-gray-300 rounded-md py-0 pl-2 pr-7\">\n <option value=\"\">Any</option>\n <option value=\"true\">Yes</option>\n <option value=\"false\">No</option>\n </select>\n }\n @case ('date') {\n <input type=\"date\" [attr.aria-label]=\"filter.label\" [ngModel]=\"filterValue(filter.key)\" (ngModelChange)=\"setFilter(filter.key, $event)\"\n class=\"h-8 text-xs border-gray-300 rounded-md py-0 px-2\" />\n }\n @case ('daterange') {\n <span class=\"flex items-center gap-1\">\n <input type=\"date\" [attr.aria-label]=\"filter.label + ' from'\" [ngModel]=\"rangePart(filter.key, 0)\" (ngModelChange)=\"setRange(filter.key, 0, $event)\"\n class=\"h-8 text-xs border-gray-300 rounded-md py-0 px-2\" />\n <span class=\"text-gray-400\">to</span>\n <input type=\"date\" [attr.aria-label]=\"filter.label + ' to'\" [ngModel]=\"rangePart(filter.key, 1)\" (ngModelChange)=\"setRange(filter.key, 1, $event)\"\n class=\"h-8 text-xs border-gray-300 rounded-md py-0 px-2\" />\n </span>\n }\n @case ('number') {\n <input type=\"number\" [attr.aria-label]=\"filter.label\" [ngModel]=\"filterValue(filter.key)\" (ngModelChange)=\"setFilter(filter.key, $event)\"\n [placeholder]=\"filter.placeholder ?? ''\" class=\"h-8 w-28 text-xs border-gray-300 rounded-md py-0 px-2\" />\n }\n @case ('numberrange') {\n <span class=\"flex items-center gap-1\">\n <input type=\"number\" placeholder=\"Min\" [attr.aria-label]=\"filter.label + ' minimum'\" [ngModel]=\"rangePart(filter.key, 0)\" (ngModelChange)=\"setRange(filter.key, 0, $event)\"\n class=\"h-8 w-24 text-xs border-gray-300 rounded-md py-0 px-2\" />\n <span class=\"text-gray-400\">to</span>\n <input type=\"number\" placeholder=\"Max\" [attr.aria-label]=\"filter.label + ' maximum'\" [ngModel]=\"rangePart(filter.key, 1)\" (ngModelChange)=\"setRange(filter.key, 1, $event)\"\n class=\"h-8 w-24 text-xs border-gray-300 rounded-md py-0 px-2\" />\n </span>\n }\n @default {\n <input type=\"text\" [attr.aria-label]=\"filter.label\" [ngModel]=\"filterValue(filter.key)\" (ngModelChange)=\"setFilter(filter.key, $event)\"\n [placeholder]=\"filter.placeholder ?? ''\" class=\"h-8 w-40 text-xs border-gray-300 rounded-md py-0 px-2\" />\n }\n }\n </div>\n }\n @if (hasFilters()) {\n <button type=\"button\" (click)=\"clearFilters()\" class=\"h-8 text-xs text-gray-500 hover:text-gray-700 hover:underline\">Reset filters</button>\n }\n </div>\n }\n }\n\n <!-- Results -->\n @if (query().length < minLength) {\n <div class=\"rounded-lg border border-dashed border-gray-200 p-10 text-center text-sm text-gray-500\">\n Type at least {{ minLength }} characters to search.\n </div>\n } @else if (loading()) {\n <div class=\"rounded-lg border border-gray-200 p-10 text-center text-sm text-gray-500\">Searching…</div>\n } @else if (response(); as result) {\n @if (result.groups.length === 0) {\n <div class=\"rounded-lg border border-dashed border-gray-200 p-10 text-center text-sm text-gray-500\">\n No matches for “{{ result.query }}”.\n </div>\n }\n <div class=\"grid gap-4\" [class.lg:grid-cols-2]=\"providerId() === null && result.groups.length > 1\">\n @for (group of result.groups; track group.providerId) {\n <section class=\"rounded-lg border border-gray-200 bg-white overflow-hidden\">\n <header class=\"flex items-center justify-between px-4 py-2.5 border-b border-gray-100 bg-gray-50/70\">\n <div class=\"flex items-center gap-2 min-w-0\">\n <span class=\"inline-flex h-6 w-6 items-center justify-center rounded\" [class]=\"moduleTone(group.module)\">\n <ui-icon [name]=\"group.icon\" class=\"h-3.5 w-3.5\" />\n </span>\n <h2 class=\"text-sm font-semibold text-gray-900 truncate\">{{ group.displayName }}</h2>\n @if (group.module !== platformModule) {\n <span class=\"text-xs text-gray-400 truncate\">· {{ moduleLabel(group.module) }}</span>\n }\n <span class=\"text-xs text-gray-400\">({{ group.totalCount }})</span>\n </div>\n <div class=\"flex items-center gap-3 shrink-0 text-xs\">\n @if (providerId() === null && group.totalCount > group.items.length) {\n <button type=\"button\" (click)=\"selectProvider(group.providerId)\" class=\"text-primary-600 hover:text-primary-700\">Show all here</button>\n }\n @if (group.seeAllRoute) {\n <a [href]=\"group.seeAllRoute\" (click)=\"go($event, group.seeAllRoute)\" class=\"text-gray-500 hover:text-gray-700\">Open list</a>\n }\n </div>\n </header>\n <ul class=\"divide-y divide-gray-100\">\n @for (item of group.items; track item.id) {\n <li>\n <a [href]=\"hrefFor(item)\" (click)=\"openItem($event, item)\"\n class=\"flex items-start gap-3 px-4 py-2.5 hover:bg-gray-50 transition-colors\">\n @if (item.imageUrl) {\n <img [src]=\"item.imageUrl\" alt=\"\" class=\"h-9 w-9 rounded-full object-cover shrink-0\" />\n } @else {\n <span class=\"inline-flex h-9 w-9 items-center justify-center rounded-full shrink-0\" [class]=\"moduleTone(group.module)\">\n <ui-icon [name]=\"item.icon ?? group.icon\" class=\"h-4 w-4\" />\n </span>\n }\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-sm font-medium text-gray-900\">{{ item.title }}</span>\n @if (item.subtitle) {\n <span class=\"block truncate text-xs text-gray-500\">{{ item.subtitle }}</span>\n }\n @if (item.description) {\n <span class=\"block truncate text-xs text-gray-400\">{{ item.description }}</span>\n }\n </span>\n @if (item.badges.length > 0) {\n <span class=\"flex flex-wrap justify-end gap-1 shrink-0 max-w-[14rem]\">\n @for (badge of item.badges; track badge.label + badge.value) {\n <span class=\"inline-flex px-1.5 py-0.5 rounded text-[11px] font-medium\" [class]=\"badgeTone(badge.tone)\" [title]=\"badge.label\">{{ badge.value }}</span>\n }\n </span>\n }\n </a>\n </li>\n }\n </ul>\n </section>\n }\n </div>\n }\n </div>\n `,\n})\nexport class SearchResultsPageComponent implements OnInit, OnDestroy {\n private readonly searchService = inject(AdminSearchService);\n private readonly tenantService = inject(TenantService);\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly destroy$ = new Subject<void>();\n\n readonly minLength = MIN_SEARCH_LENGTH;\n readonly platformModule = PLATFORM_MODULE;\n\n draft = '';\n readonly query = signal('');\n readonly providerId = signal<string | null>(null);\n readonly filters = signal<Record<string, string>>({});\n readonly providers = signal<AdminSearchProvider[]>([]);\n readonly modules = signal<Record<string, RegisteredModuleInfo>>({});\n readonly response = signal<AdminSearchResponse | null>(null);\n readonly loading = signal(false);\n\n readonly selectedProvider = computed(() => this.providers().find(p => p.providerId === this.providerId()) ?? null);\n readonly hasFilters = computed(() => Object.values(this.filters()).some(v => v !== ''));\n readonly providerGroups = computed(() => groupProvidersByModule(this.providers(), m => this.moduleLabel(m)));\n readonly placeholder = computed(() => {\n const provider = this.selectedProvider();\n if (!provider) return 'Search everything you can see';\n const scope = provider.module === PLATFORM_MODULE ? '' : `${this.moduleLabel(provider.module)} `;\n return `Search ${scope}${provider.displayName.toLowerCase()}`;\n });\n\n ngOnInit(): void {\n this.searchService.getProviders().pipe(takeUntil(this.destroy$)).subscribe(providers => this.providers.set(providers));\n this.tenantService.getRegisteredModules().pipe(takeUntil(this.destroy$)).subscribe(response => {\n const byKey: Record<string, RegisteredModuleInfo> = {};\n for (const module of response.modules) byKey[module.featureKey] = module;\n this.modules.set(byKey);\n });\n\n this.route.queryParamMap\n .pipe(\n switchMap(params => {\n const query = (params.get('q') ?? '').trim();\n const providerId = params.get('provider');\n const filters: Record<string, string> = {};\n for (const key of params.keys) {\n if (key.startsWith('f.')) filters[key.slice(2)] = params.get(key) ?? '';\n }\n this.query.set(query);\n this.draft = query;\n this.providerId.set(providerId);\n this.filters.set(filters);\n if (query.length < MIN_SEARCH_LENGTH) {\n this.response.set(null);\n this.loading.set(false);\n return of(null);\n }\n this.loading.set(true);\n return this.searchService.search(query, { providerId: providerId ?? undefined, filters, limit: providerId ? 50 : 10 })\n .pipe(catchError(() => of({ query, totalCount: 0, groups: [] } as AdminSearchResponse)));\n }),\n takeUntil(this.destroy$),\n )\n .subscribe(response => {\n this.response.set(response);\n this.loading.set(false);\n });\n }\n\n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n submit(event: Event): void {\n event.preventDefault();\n this.apply(this.draft.trim(), this.providerId(), this.filters());\n }\n\n /** Emptying the box, by typing or the field's clear button, drops the results straight away. */\n onDraftChange(value: string): void {\n this.draft = value;\n if (value.trim() === '' && this.query() !== '') {\n this.apply('', this.providerId(), this.filters());\n }\n }\n\n selectProvider(providerId: string | null): void {\n this.apply(this.query(), providerId, {});\n }\n\n filterValue(key: string): string {\n return this.filters()[key] ?? '';\n }\n\n filterValues(key: string): string[] {\n const value = this.filterValue(key);\n return value ? value.split(',') : [];\n }\n\n setFilter(key: string, value: string | null): void {\n this.apply(this.query(), this.providerId(), { ...this.filters(), [key]: value ?? '' });\n }\n\n rangePart(key: string, index: 0 | 1): string {\n return this.filterValue(key).split('..')[index] ?? '';\n }\n\n setRange(key: string, index: 0 | 1, value: string | number | null): void {\n const parts = this.filterValue(key).split('..');\n parts[index] = value == null ? '' : String(value);\n const joined = `${parts[0] ?? ''}..${parts[1] ?? ''}`;\n this.setFilter(key, joined === '..' ? '' : joined);\n }\n\n clearFilters(): void {\n this.apply(this.query(), this.providerId(), {});\n }\n\n moduleLabel(module: string): string {\n return this.modules()[module]?.displayName ?? MODULE_LABEL_FALLBACKS[module] ?? module;\n }\n\n moduleTone(module: string): string {\n return moduleTone(this.modules()[module]?.color);\n }\n\n badgeTone(tone: AdminSearchBadgeTone): string {\n return badgeTone(tone);\n }\n\n hrefFor(item: AdminSearchResultItem): string {\n return item.route || workspaceHref(item.workspaceRoute) || '#';\n }\n\n openItem(event: MouseEvent, item: AdminSearchResultItem): void {\n if (item.route) {\n event.preventDefault();\n this.router.navigateByUrl(item.route);\n return;\n }\n if (!workspaceHref(item.workspaceRoute)) event.preventDefault();\n }\n\n go(event: MouseEvent, route: string): void {\n event.preventDefault();\n this.router.navigateByUrl(route);\n }\n\n private apply(query: string, providerId: string | null, filters: Record<string, string>): void {\n this.router.navigate([], { relativeTo: this.route, queryParams: searchPageParams(query, providerId, filters) });\n }\n}\n"],"names":[],"mappings":"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"}