@conduction/nextcloud-vue 2.117.0 → 2.118.0

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 (97) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +2 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +6 -6
  5. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +2 -1
  7. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  8. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  9. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +18 -0
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +14 -8
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  14. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -1
  15. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  17. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +9 -4
  18. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  19. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnMailTab/CnMailTab.vue2.js +2 -1
  21. package/dist/esm/components/CnMailTab/CnMailTab.vue2.js.map +1 -1
  22. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  23. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +71 -0
  24. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  25. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +25 -9
  26. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  27. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  28. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +11 -2
  29. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  30. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  31. package/dist/esm/components/CnSearchPage/CnSearchPage.vue.js +1 -1
  32. package/dist/esm/components/CnSearchPage/CnSearchPage.vue2.js +161 -5
  33. package/dist/esm/components/CnSearchPage/CnSearchPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnSearchPage/CnSearchPage.vue3.js +9 -9
  35. package/dist/esm/components/CnSearchPage/CnSearchPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  37. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +35 -5
  38. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  40. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue.js +9 -0
  41. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue.js.map +1 -0
  42. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js +435 -0
  43. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js.map +1 -0
  44. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js +300 -0
  45. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js.map +1 -0
  46. package/dist/esm/composables/useEndpointSource.js +1 -36
  47. package/dist/esm/composables/useEndpointSource.js.map +1 -1
  48. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  49. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +57 -336
  50. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  51. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +18 -155
  52. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  53. package/dist/esm/index.js +2 -0
  54. package/dist/esm/index.js.map +1 -1
  55. package/dist/esm/l10n/en.json.js +2 -1
  56. package/dist/esm/l10n/en.json.js.map +1 -1
  57. package/dist/esm/l10n/nl.json.js +2 -1
  58. package/dist/esm/l10n/nl.json.js.map +1 -1
  59. package/dist/esm/library-style.js +1 -1
  60. package/dist/esm/nextcloud-vue.css +308 -229
  61. package/dist/esm/utils/actionsDispatcher.js +3 -2
  62. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  63. package/dist/esm/utils/interpolateUrlTokens.js +43 -0
  64. package/dist/esm/utils/interpolateUrlTokens.js.map +1 -0
  65. package/dist/esm/utils/labelsByField.js +72 -0
  66. package/dist/esm/utils/labelsByField.js.map +1 -0
  67. package/dist/esm/utils/listCreateAction.js +91 -0
  68. package/dist/esm/utils/listCreateAction.js.map +1 -0
  69. package/dist/esm/utils/transitionInputs.js +493 -0
  70. package/dist/esm/utils/transitionInputs.js.map +1 -0
  71. package/dist/esm/utils/visibleWhen.js +9 -2
  72. package/dist/esm/utils/visibleWhen.js.map +1 -1
  73. package/dist/nextcloud-vue.cjs.js +6690 -5404
  74. package/dist/nextcloud-vue.cjs.js.map +2 -2
  75. package/dist/nextcloud-vue.css +308 -229
  76. package/l10n/en.json +2 -1
  77. package/l10n/nl.json +2 -1
  78. package/package.json +1 -1
  79. package/src/components/CnActionsBar/CnActionsBar.vue +6 -6
  80. package/src/components/CnDashboardPage/CnDashboardPage.vue +27 -5
  81. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +11 -1
  82. package/src/components/CnIndexPage/CnIndexPage.vue +9 -4
  83. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +86 -0
  84. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +11 -2
  85. package/src/components/CnSearchPage/CnSearchPage.vue +170 -14
  86. package/src/components/CnTabsWidget/CnTabsWidget.vue +35 -5
  87. package/src/components/CnTransitionInputFields/CnTransitionInputFields.vue +750 -0
  88. package/src/components/CnTransitionInputFields/index.js +11 -0
  89. package/src/components/index.js +1 -0
  90. package/src/composables/useEndpointSource.js +3 -36
  91. package/src/dialogs/CnTransitionInputDialog.vue +73 -465
  92. package/src/index.js +2 -0
  93. package/src/utils/interpolateUrlTokens.js +40 -0
  94. package/src/utils/labelsByField.js +69 -0
  95. package/src/utils/listCreateAction.js +87 -0
  96. package/src/utils/transitionInputs.js +489 -0
  97. package/src/utils/visibleWhen.js +9 -2
@@ -96,6 +96,20 @@ var script = {
96
96
  type: Function,
97
97
  default: (total, shown) => `Showing ${shown} of ${total} results.`,
98
98
  },
99
+
100
+ /**
101
+ * The lists the page searches itself (manifest `config.sources`). Each
102
+ * `{ id?, label, register, schema, route?, titleField?, snippetField?,
103
+ * limit? }`: one request per list with `_search`, results in the order
104
+ * of the lists, and the lists offered as a Type filter with their
105
+ * totals. `route` names the page a result opens, with its id. Empty
106
+ * keeps the page controlled: `results` and `facets` come from props.
107
+ *
108
+ * @type {Array<object>}
109
+ */
110
+ sources: { type: Array, default: () => [] },
111
+ /** The Type filter's heading when the page searches `sources`. */
112
+ typeLabel: { type: String, default: 'Type' },
99
113
  },
100
114
 
101
115
  emits: ['facets-change', 'query-change', 'result-click', 'search', 'update:query'],
@@ -103,18 +117,88 @@ var script = {
103
117
  return {
104
118
  localQuery: this.query,
105
119
  hasSearched: false,
120
+ // What a `sources` search found, per list, and the filter it keeps.
121
+ sourceRows: {},
122
+ sourceTotals: {},
123
+ sourceFacets: {},
124
+ sourceLoading: false,
125
+ sourceRun: 0,
106
126
  }
107
127
  },
108
128
 
109
129
  computed: {
130
+ /** @return {boolean} Whether the page searches its own `sources`. */
131
+ searchesSources() {
132
+ return Array.isArray(this.sources) && this.sources.some((source) => source && source.register && source.schema)
133
+ },
134
+
135
+ /** @return {Array<object>} The lists searched, each with its key. */
136
+ sourceList() {
137
+ return (this.sources || [])
138
+ .filter((source) => source && source.register && source.schema)
139
+ .map((source) => ({ ...source, key: source.id || source.schema }))
140
+ },
141
+
142
+ /** @return {Record<string, Array<string>>} The active filter, the page's own when it searches `sources`. */
143
+ effectiveFacets() {
144
+ return this.searchesSources ? this.sourceFacets : (this.activeFacets || {})
145
+ },
146
+
147
+ /** @return {Array<object>} The facets shown: the Type filter of the lists, else the `facets` prop. */
148
+ shownFacets() {
149
+ if (!this.searchesSources) {
150
+ return this.facets
151
+ }
152
+ if (!this.hasSearched) {
153
+ return []
154
+ }
155
+ return [{
156
+ key: 'type',
157
+ label: this.typeLabel,
158
+ multiple: true,
159
+ options: this.sourceList.map((source) => ({
160
+ value: source.key,
161
+ label: source.label,
162
+ ...(typeof this.sourceTotals[source.key] === 'number' ? { count: this.sourceTotals[source.key] } : {}),
163
+ })),
164
+ }]
165
+ },
166
+
167
+ /** @return {Array<object>} The results shown. */
168
+ shownResults() {
169
+ if (!this.searchesSources) {
170
+ return this.results
171
+ }
172
+ return this.activeSources.flatMap((source) => this.sourceRows[source.key] || [])
173
+ },
174
+
175
+ /** @return {number} The total of the results shown. */
176
+ shownTotal() {
177
+ if (!this.searchesSources) {
178
+ return this.totalCount
179
+ }
180
+ return this.activeSources.reduce((sum, source) => sum + (this.sourceTotals[source.key] || 0), 0)
181
+ },
182
+
183
+ /** @return {boolean} Whether a search is running. */
184
+ shownLoading() {
185
+ return this.searchesSources ? this.sourceLoading : this.loading
186
+ },
187
+
188
+ /** @return {Array<object>} The lists the Type filter leaves in, all when none is picked. */
189
+ activeSources() {
190
+ const picked = Array.isArray(this.effectiveFacets.type) ? this.effectiveFacets.type : [];
191
+ return picked.length === 0 ? this.sourceList : this.sourceList.filter((source) => picked.includes(source.key))
192
+ },
193
+
110
194
  /**
111
195
  * Whether any facet has at least one active value.
112
196
  *
113
197
  * @return {boolean}
114
198
  */
115
199
  hasActiveFacets() {
116
- for (const k of Object.keys(this.activeFacets || {})) {
117
- if (Array.isArray(this.activeFacets[k]) && this.activeFacets[k].length > 0) {
200
+ for (const k of Object.keys(this.effectiveFacets)) {
201
+ if (Array.isArray(this.effectiveFacets[k]) && this.effectiveFacets[k].length > 0) {
118
202
  return true
119
203
  }
120
204
  }
@@ -159,7 +243,7 @@ var script = {
159
243
  * @return {boolean}
160
244
  */
161
245
  isFacetActive(key, value) {
162
- const arr = this.activeFacets[key];
246
+ const arr = this.effectiveFacets[key];
163
247
  return Array.isArray(arr) && arr.includes(value)
164
248
  },
165
249
 
@@ -173,7 +257,7 @@ var script = {
173
257
  * @return {void}
174
258
  */
175
259
  toggleFacet(key, value, checked, multiple) {
176
- const next = { ...this.activeFacets };
260
+ const next = { ...this.effectiveFacets };
177
261
  if (!multiple) {
178
262
  next[key] = checked ? [value] : [];
179
263
  } else {
@@ -204,6 +288,9 @@ var script = {
204
288
  * @type {Record<string,Array<string>>}
205
289
  */
206
290
  this.$emit('facets-change', next);
291
+ if (this.searchesSources) {
292
+ this.sourceFacets = next;
293
+ }
207
294
  this.emitSearch();
208
295
  },
209
296
 
@@ -244,7 +331,73 @@ var script = {
244
331
  * Combines query + active facets.
245
332
  * @type {{ query: string, facets: Record<string,Array<string>> }}
246
333
  */
247
- this.$emit('search', { query: this.localQuery, facets: { ...this.activeFacets } });
334
+ this.$emit('search', { query: this.localQuery, facets: { ...this.effectiveFacets } });
335
+ if (this.searchesSources) {
336
+ this.searchSources();
337
+ }
338
+ },
339
+
340
+ /**
341
+ * Search the lists the Type filter leaves in, one request each, and
342
+ * keep what each found. A search started later wins over this one.
343
+ *
344
+ * @return {Promise<void>}
345
+ */
346
+ async searchSources() {
347
+ const run = this.sourceRun + 1;
348
+ this.sourceRun = run;
349
+ this.sourceLoading = true;
350
+ const [{ default: axios }, { generateUrl }] = await Promise.all([
351
+ import('@nextcloud/axios'),
352
+ import('@nextcloud/router'),
353
+ ]);
354
+ const found = await Promise.all(this.activeSources.map(async (source) => {
355
+ const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: source.register, schema: source.schema });
356
+ const limit = Number(source.limit) > 0 ? Math.floor(Number(source.limit)) : 10;
357
+ try {
358
+ const response = await axios.get(url, { params: { _search: this.localQuery, _limit: limit } });
359
+ const data = response && response.data ? response.data : {};
360
+ const rows = Array.isArray(data.results) ? data.results : [];
361
+ return { source, rows, total: typeof data.total === 'number' ? data.total : rows.length }
362
+ } catch (e) {
363
+ // eslint-disable-next-line no-console
364
+ console.warn('[CnSearchPage] could not search', source.schema, e);
365
+ return { source, rows: [], total: 0 }
366
+ }
367
+ }));
368
+ if (run !== this.sourceRun) {
369
+ return
370
+ }
371
+ const rows = { ...this.sourceRows };
372
+ const totals = { ...this.sourceTotals };
373
+ for (const { source, rows: list, total } of found) {
374
+ rows[source.key] = list.map((row) => this.resultOf(source, row));
375
+ totals[source.key] = total;
376
+ }
377
+ this.sourceRows = rows;
378
+ this.sourceTotals = totals;
379
+ this.sourceLoading = false;
380
+ },
381
+
382
+ /**
383
+ * One found object as a result row.
384
+ *
385
+ * @param {object} source The list it came from.
386
+ * @param {object} row The object.
387
+ * @return {object} `{ id, title, snippet, schema, route }`.
388
+ */
389
+ resultOf(source, row) {
390
+ const id = row && (row.id || row['@self']?.id);
391
+ const titleField = source.titleField || '';
392
+ const title = (titleField && row[titleField]) || row.title || row.name || id;
393
+ const snippet = source.snippetField ? row[source.snippetField] : undefined;
394
+ return {
395
+ id,
396
+ title: typeof title === 'string' ? title : String(title ?? ''),
397
+ ...(typeof snippet === 'string' && snippet !== '' ? { snippet } : {}),
398
+ schema: this.shown(source.label) || source.schema,
399
+ route: source.route || '',
400
+ }
248
401
  },
249
402
 
250
403
  /**
@@ -259,6 +412,9 @@ var script = {
259
412
  * @type {object}
260
413
  */
261
414
  this.$emit('result-click', result);
415
+ if (this.searchesSources && result && result.route && result.id && this.$router) {
416
+ this.$router.push({ name: result.route, params: { id: result.id } });
417
+ }
262
418
  },
263
419
  },
264
420
  };
@@ -1 +1 @@
1
- {"version":3,"file":"CnSearchPage.vue2.js","sources":["../../../../src/components/CnSearchPage/CnSearchPage.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-search-page\" data-testid=\"cn-search-page\">\n\t\t<header class=\"cn-search-page__header\">\n\t\t\t<h2 v-if=\"title\" class=\"cn-search-page__title\" :lang=\"langOf(title)\">\n\t\t\t\t{{ shown(title) }}\n\t\t\t</h2>\n\t\t\t<form class=\"cn-search-page__query\" @submit.prevent=\"onQuerySubmit\">\n\t\t\t\t<input\n\t\t\t\t\tv-model=\"localQuery\"\n\t\t\t\t\ttype=\"search\"\n\t\t\t\t\t:placeholder=\"placeholder\"\n\t\t\t\t\t:aria-label=\"ariaLabel\"\n\t\t\t\t\tclass=\"cn-search-page__input\"\n\t\t\t\t\tdata-testid=\"cn-search-page-input\"\n\t\t\t\t\t@input=\"onQueryInput\">\n\t\t\t\t<button type=\"submit\" class=\"cn-search-page__submit\">\n\t\t\t\t\t{{ searchLabel }}\n\t\t\t\t</button>\n\t\t\t</form>\n\t\t</header>\n\n\t\t<div class=\"cn-search-page__body\">\n\t\t\t<!-- Facet sidebar. -->\n\t\t\t<aside v-if=\"facets.length > 0\" class=\"cn-search-page__facets\" data-testid=\"cn-search-page-facets\">\n\t\t\t\t<h3 class=\"cn-search-page__facets-title\">\n\t\t\t\t\t{{ facetsTitle }}\n\t\t\t\t</h3>\n\t\t\t\t<div v-for=\"facet in facets\" :key=\"facet.key\" class=\"cn-search-page__facet\">\n\t\t\t\t\t<h4 class=\"cn-search-page__facet-title\">\n\t\t\t\t\t\t{{ facet.label || facet.key }}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<ul class=\"cn-search-page__facet-options\">\n\t\t\t\t\t\t<li v-for=\"opt in facet.options\"\n\t\t\t\t\t\t\t:key=\"opt.value\"\n\t\t\t\t\t\t\tclass=\"cn-search-page__facet-option\">\n\t\t\t\t\t\t\t<label>\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t:model-value=\"isFacetActive(facet.key, opt.value)\"\n\t\t\t\t\t\t\t\t\t:type=\"facet.multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t\t\t\t:name=\"'facet-' + facet.key\"\n\t\t\t\t\t\t\t\t\t@change=\"toggleFacet(facet.key, opt.value, $event.target.checked, facet.multiple)\">\n\t\t\t\t\t\t\t\t<span>{{ opt.label || opt.value }}</span>\n\t\t\t\t\t\t\t\t<small v-if=\"typeof opt.count === 'number'\" class=\"cn-search-page__facet-count\">{{ opt.count }}</small>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</div>\n\t\t\t\t<button v-if=\"hasActiveFacets\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-search-page__clear-facets\"\n\t\t\t\t\t@click=\"clearFacets\">\n\t\t\t\t\t{{ clearFacetsLabel }}\n\t\t\t\t</button>\n\t\t\t</aside>\n\n\t\t\t<!-- Results pane. -->\n\t\t\t<section class=\"cn-search-page__results\" data-testid=\"cn-search-page-results\">\n\t\t\t\t<div v-if=\"loading\" class=\"cn-search-page__loading\">\n\t\t\t\t\t{{ loadingLabel }}\n\t\t\t\t</div>\n\t\t\t\t<ol v-else-if=\"results.length > 0\" class=\"cn-search-page__list\">\n\t\t\t\t\t<li v-for=\"result in results\"\n\t\t\t\t\t\t:key=\"result.id || (result.schema + ':' + (result.title || ''))\"\n\t\t\t\t\t\tclass=\"cn-search-page__result\"\n\t\t\t\t\t\t:data-result-id=\"result.id\"\n\t\t\t\t\t\t@click=\"onResultClick(result)\">\n\t\t\t\t\t\t<!-- @slot result Per-result body. Scope:\n\t\t\t\t\t\t { result }. Default renders title +\n\t\t\t\t\t\t snippet + schema badge. -->\n\t\t\t\t\t\t<slot name=\"result\" :result=\"result\">\n\t\t\t\t\t\t\t<div class=\"cn-search-page__result-head\">\n\t\t\t\t\t\t\t\t<span class=\"cn-search-page__result-title\">{{ result.title || result.id }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"result.schema\" class=\"cn-search-page__result-badge\">{{ result.schema }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<p v-if=\"result.snippet\" class=\"cn-search-page__result-snippet\">\n\t\t\t\t\t\t\t\t{{ result.snippet }}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<small v-if=\"result.subtitle\" class=\"cn-search-page__result-subtitle\">{{ result.subtitle }}</small>\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</li>\n\t\t\t\t</ol>\n\t\t\t\t<p v-else-if=\"hasSearched\" class=\"cn-search-page__empty\">\n\t\t\t\t\t{{ emptyLabel }}\n\t\t\t\t</p>\n\t\t\t\t<p v-else-if=\"idleLabel\" class=\"cn-search-page__idle\">\n\t\t\t\t\t{{ idleLabel }}\n\t\t\t\t</p>\n\n\t\t\t\t<div v-if=\"totalCount > 0 && totalCount > results.length\" class=\"cn-search-page__more\">\n\t\t\t\t\t{{ totalCountLabel(totalCount, results.length) }}\n\t\t\t\t</div>\n\t\t\t</section>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { labelLang } from '../../utils/manifestTranslate.js'\n/**\n * CnSearchPage — Search surface with a query input, a facet\n * sidebar, and a results list. Mounted by CnPageRenderer when a\n * manifest page declares `type: 'search'`.\n *\n * The component owns the UI; consumers wire the actual search +\n * facet computation in `@query-change` and `@facets-change` (or use\n * the single `@search` event that fires on both). Cross-schema\n * search, full-text indexing, and facet aggregation are all\n * consumer-side.\n *\n * ```vue\n * <CnSearchPage\n * title=\"Search\"\n * :query=\"q\"\n * :facets=\"facets\"\n * :active-facets=\"activeFacets\"\n * :results=\"results\"\n * :total-count=\"total\"\n * :loading=\"loading\"\n * @search=\"onSearch\" />\n * ```\n */\nexport default {\n\tname: 'CnSearchPage',\n\n\tinject: {\n\t\t/** The label lookup from CnAppRoot, so manifest labels show in the user's language. */\n\t\tcnTranslate: { default: null },\n\t},\n\n\tprops: {\n\t\t/** Optional title rendered above the query input. */\n\t\ttitle: { type: String, default: 'Search' },\n\t\t/**\n\t\t * Controlled query string (v-model style). Falls back to\n\t\t * the local state when omitted; consumers can read the\n\t\t * current value via `@query-change`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tquery: { type: String, default: '' },\n\t\t/** Query input placeholder. */\n\t\tplaceholder: { type: String, default: 'Search…' },\n\t\t/** Query input aria-label. */\n\t\tariaLabel: { type: String, default: 'Search input' },\n\t\t/** Submit-button label. */\n\t\tsearchLabel: { type: String, default: 'Search' },\n\t\t/**\n\t\t * Facet declarations. Each entry: `{ key, label?,\n\t\t * options: [{value, label?, count?}], multiple? }`.\n\t\t * Empty array hides the facet sidebar.\n\t\t *\n\t\t * @type {Array<{key:string,label?:string,options:Array,multiple?:boolean}>}\n\t\t */\n\t\tfacets: { type: Array, default: () => [] },\n\t\t/**\n\t\t * Controlled active-facets map (`{facetKey: [value, ...]}`).\n\t\t * Consumers persist this + push updates back; the widget\n\t\t * emits `@facets-change` with the new map.\n\t\t *\n\t\t * @type {Record<string,Array<string>>}\n\t\t */\n\t\tactiveFacets: { type: Object, default: () => ({}) },\n\t\t/**\n\t\t * Result entries. Each entry rendered through the `#result`\n\t\t * slot or the default `{title, snippet, schema, subtitle}`\n\t\t * shape.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tresults: { type: Array, default: () => [] },\n\t\t/** Total count of matching results (drives the \"more\" footer). */\n\t\ttotalCount: { type: Number, default: 0 },\n\t\t/** Show the loading state. */\n\t\tloading: { type: Boolean, default: false },\n\t\t/** Empty-state when a search ran but matched nothing. */\n\t\temptyLabel: { type: String, default: 'No matches found.' },\n\t\t/** Idle text shown before any search has run. Empty hides. */\n\t\tidleLabel: { type: String, default: 'Start typing to search.' },\n\t\t/** Loading-state text. */\n\t\tloadingLabel: { type: String, default: 'Searching…' },\n\t\t/** Facets sidebar header. */\n\t\tfacetsTitle: { type: String, default: 'Filters' },\n\t\t/** Clear-facets button label. */\n\t\tclearFacetsLabel: { type: String, default: 'Clear filters' },\n\t\t/**\n\t\t * Formatter for the \"Showing X of Y\" footer. Receives total\n\t\t * + shown counts and returns the rendered string.\n\t\t *\n\t\t * @type {(total:number, shown:number) => string}\n\t\t */\n\t\ttotalCountLabel: {\n\t\t\ttype: Function,\n\t\t\tdefault: (total, shown) => `Showing ${shown} of ${total} results.`,\n\t\t},\n\t},\n\n\temits: ['facets-change', 'query-change', 'result-click', 'search', 'update:query'],\n\tdata() {\n\t\treturn {\n\t\t\tlocalQuery: this.query,\n\t\t\thasSearched: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether any facet has at least one active value.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasActiveFacets() {\n\t\t\tfor (const k of Object.keys(this.activeFacets || {})) {\n\t\t\t\tif (Array.isArray(this.activeFacets[k]) && this.activeFacets[k].length > 0) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\t},\n\n\twatch: {\n\t\tquery(next) {\n\t\t\tif (next !== this.localQuery) {\n\t\t\t\tthis.localQuery = next\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest label through the injected lookup, or as written.\n\t\t *\n\t\t * @param {string} text The label as written.\n\t\t * @return {string} The label in the user's language.\n\t\t */\n\t\tshown(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' && typeof text === 'string' && text !== '' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The `lang` of a label that fell back to its written text in another language.\n\t\t *\n\t\t * @param {string} text The label as written.\n\t\t * @return {string|undefined} The source language, or undefined.\n\t\t */\n\t\tlangOf(text) {\n\t\t\treturn labelLang(this.cnTranslate, text) || undefined\n\t\t},\n\n\t\t/**\n\t\t * Whether a (facetKey, value) is currently active.\n\t\t *\n\t\t * @param {string} key Facet key.\n\t\t * @param {string} value Option value.\n\t\t * @return {boolean}\n\t\t */\n\t\tisFacetActive(key, value) {\n\t\t\tconst arr = this.activeFacets[key]\n\t\t\treturn Array.isArray(arr) && arr.includes(value)\n\t\t},\n\n\t\t/**\n\t\t * Toggle a facet option on/off and emit the new map.\n\t\t *\n\t\t * @param {string} key Facet key.\n\t\t * @param {string} value Option value.\n\t\t * @param {boolean} checked Checkbox state (radios pass true).\n\t\t * @param {boolean} multiple Whether the facet allows multi-select.\n\t\t * @return {void}\n\t\t */\n\t\ttoggleFacet(key, value, checked, multiple) {\n\t\t\tconst next = { ...this.activeFacets }\n\t\t\tif (!multiple) {\n\t\t\t\tnext[key] = checked ? [value] : []\n\t\t\t} else {\n\t\t\t\tconst cur = Array.isArray(next[key]) ? [...next[key]] : []\n\t\t\t\tconst idx = cur.indexOf(value)\n\t\t\t\tif (checked && idx < 0) {\n\t\t\t\t\tcur.push(value)\n\t\t\t\t} else if (!checked && idx >= 0) {\n\t\t\t\t\tcur.splice(idx, 1)\n\t\t\t\t}\n\t\t\t\tnext[key] = cur\n\t\t\t}\n\t\t\tthis.emitFacetsChange(next)\n\t\t},\n\n\t\t/**\n\t\t * Clear every facet.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclearFacets() {\n\t\t\tthis.emitFacetsChange({})\n\t\t},\n\n\t\temitFacetsChange(next) {\n\t\t\t/**\n\t\t\t * @event facets-change Emitted on any facet toggle / clear.\n\t\t\t * @type {Record<string,Array<string>>}\n\t\t\t */\n\t\t\tthis.$emit('facets-change', next)\n\t\t\tthis.emitSearch()\n\t\t},\n\n\t\t/**\n\t\t * Submit-button handler — propagates the latest query.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonQuerySubmit() {\n\t\t\tthis.emitQueryChange()\n\t\t\tthis.emitSearch()\n\t\t},\n\n\t\t/**\n\t\t * Live query-input handler — emits each keystroke (consumers\n\t\t * are responsible for debouncing).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonQueryInput() {\n\t\t\tthis.emitQueryChange()\n\t\t},\n\n\t\temitQueryChange() {\n\t\t\t/**\n\t\t\t * @event query-change Emitted on every query mutation.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('query-change', this.localQuery)\n\t\t\t/** v-model-friendly alias. */\n\t\t\tthis.$emit('update:query', this.localQuery)\n\t\t},\n\n\t\temitSearch() {\n\t\t\tthis.hasSearched = true\n\t\t\t/**\n\t\t\t * @event search Emitted on Submit or facet change.\n\t\t\t * Combines query + active facets.\n\t\t\t * @type {{ query: string, facets: Record<string,Array<string>> }}\n\t\t\t */\n\t\t\tthis.$emit('search', { query: this.localQuery, facets: { ...this.activeFacets } })\n\t\t},\n\n\t\t/**\n\t\t * Forward a result click.\n\t\t *\n\t\t * @param {object} result The clicked result.\n\t\t * @return {void}\n\t\t */\n\t\tonResultClick(result) {\n\t\t\t/**\n\t\t\t * @event result-click Emitted on result-row click.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('result-click', result)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-search-page {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\tpadding: 16px;\n}\n\n/* Reserve room for NcAppNavigationToggle (matches CnPageRenderer / CnPageHeader). */\n.cn-search-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-search-page__title {\n\tmargin: 0 0 8px;\n\tfont-size: 1.4em;\n}\n\n.cn-search-page__query {\n\tdisplay: flex;\n\tgap: 8px;\n}\n\n.cn-search-page__input {\n\tflex: 1 1 auto;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tfont-size: 1em;\n}\n\n.cn-search-page__submit {\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text, #fff);\n\tborder: none;\n\tpadding: 8px 16px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n}\n\n.cn-search-page__body {\n\tdisplay: grid;\n\tgrid-template-columns: 240px 1fr;\n\tgap: 16px;\n}\n\n.cn-search-page__body:has(:not(.cn-search-page__facets)) {\n\tgrid-template-columns: 1fr;\n}\n\n.cn-search-page__facets {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 12px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-search-page__facets-title {\n\tmargin: 0;\n\tfont-size: 1em;\n}\n\n.cn-search-page__facet-title {\n\tmargin: 0 0 4px;\n\tfont-size: 0.9em;\n}\n\n.cn-search-page__facet-options {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-search-page__facet-option {\n\tdisplay: flex;\n\tgap: 4px;\n}\n\n.cn-search-page__facet-option label {\n\tdisplay: flex;\n\tgap: 6px;\n\talign-items: center;\n\tcursor: pointer;\n\tfont-size: 0.9em;\n}\n\n.cn-search-page__facet-count {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-search-page__clear-facets {\n\tbackground: none;\n\tborder: 1px solid var(--color-border);\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tfont-size: 0.85em;\n\talign-self: flex-start;\n}\n\n.cn-search-page__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-search-page__result {\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tbackground: var(--color-main-background);\n}\n\n.cn-search-page__result:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-search-page__result-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-search-page__result-title {\n\tfont-weight: 600;\n}\n\n.cn-search-page__result-badge {\n\tfont-size: 0.75em;\n\tpadding: 1px 8px;\n\tborder-radius: 9999px;\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\ttext-transform: uppercase;\n\tletter-spacing: 0.05em;\n}\n\n.cn-search-page__result-snippet {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-search-page__result-subtitle {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-search-page__empty,\n.cn-search-page__idle,\n.cn-search-page__loading {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 24px 0;\n\ttext-align: center;\n\tfont-style: italic;\n}\n\n.cn-search-page__more {\n\tmargin-top: 12px;\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n}\n</style>\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CnSearchPage.vue2.js","sources":["../../../../src/components/CnSearchPage/CnSearchPage.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-search-page\" data-testid=\"cn-search-page\">\n\t\t<header class=\"cn-search-page__header\">\n\t\t\t<h2 v-if=\"title\" class=\"cn-search-page__title\" :lang=\"langOf(title)\">\n\t\t\t\t{{ shown(title) }}\n\t\t\t</h2>\n\t\t\t<form class=\"cn-search-page__query\" @submit.prevent=\"onQuerySubmit\">\n\t\t\t\t<input\n\t\t\t\t\tv-model=\"localQuery\"\n\t\t\t\t\ttype=\"search\"\n\t\t\t\t\t:placeholder=\"placeholder\"\n\t\t\t\t\t:aria-label=\"ariaLabel\"\n\t\t\t\t\tclass=\"cn-search-page__input\"\n\t\t\t\t\tdata-testid=\"cn-search-page-input\"\n\t\t\t\t\t@input=\"onQueryInput\">\n\t\t\t\t<button type=\"submit\" class=\"cn-search-page__submit\">\n\t\t\t\t\t{{ searchLabel }}\n\t\t\t\t</button>\n\t\t\t</form>\n\t\t</header>\n\n\t\t<div class=\"cn-search-page__body\">\n\t\t\t<!-- Facet sidebar. -->\n\t\t\t<aside v-if=\"shownFacets.length > 0\" class=\"cn-search-page__facets\" data-testid=\"cn-search-page-facets\">\n\t\t\t\t<h3 class=\"cn-search-page__facets-title\">\n\t\t\t\t\t{{ facetsTitle }}\n\t\t\t\t</h3>\n\t\t\t\t<div v-for=\"facet in shownFacets\" :key=\"facet.key\" class=\"cn-search-page__facet\">\n\t\t\t\t\t<h4 class=\"cn-search-page__facet-title\">\n\t\t\t\t\t\t{{ shown(facet.label) || facet.key }}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<ul class=\"cn-search-page__facet-options\">\n\t\t\t\t\t\t<li v-for=\"opt in facet.options\"\n\t\t\t\t\t\t\t:key=\"opt.value\"\n\t\t\t\t\t\t\tclass=\"cn-search-page__facet-option\">\n\t\t\t\t\t\t\t<label>\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t:model-value=\"isFacetActive(facet.key, opt.value)\"\n\t\t\t\t\t\t\t\t\t:type=\"facet.multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t\t\t\t:name=\"'facet-' + facet.key\"\n\t\t\t\t\t\t\t\t\t@change=\"toggleFacet(facet.key, opt.value, $event.target.checked, facet.multiple)\">\n\t\t\t\t\t\t\t\t<span>{{ shown(opt.label) || opt.value }}</span>\n\t\t\t\t\t\t\t\t<small v-if=\"typeof opt.count === 'number'\" class=\"cn-search-page__facet-count\">{{ opt.count }}</small>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</div>\n\t\t\t\t<button v-if=\"hasActiveFacets\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-search-page__clear-facets\"\n\t\t\t\t\t@click=\"clearFacets\">\n\t\t\t\t\t{{ clearFacetsLabel }}\n\t\t\t\t</button>\n\t\t\t</aside>\n\n\t\t\t<!-- Results pane. -->\n\t\t\t<section class=\"cn-search-page__results\" data-testid=\"cn-search-page-results\">\n\t\t\t\t<div v-if=\"shownLoading\" class=\"cn-search-page__loading\">\n\t\t\t\t\t{{ loadingLabel }}\n\t\t\t\t</div>\n\t\t\t\t<ol v-else-if=\"shownResults.length > 0\" class=\"cn-search-page__list\">\n\t\t\t\t\t<li v-for=\"result in shownResults\"\n\t\t\t\t\t\t:key=\"result.id || (result.schema + ':' + (result.title || ''))\"\n\t\t\t\t\t\tclass=\"cn-search-page__result\"\n\t\t\t\t\t\t:data-result-id=\"result.id\"\n\t\t\t\t\t\t@click=\"onResultClick(result)\">\n\t\t\t\t\t\t<!-- @slot result Per-result body. Scope:\n\t\t\t\t\t\t { result }. Default renders title +\n\t\t\t\t\t\t snippet + schema badge. -->\n\t\t\t\t\t\t<slot name=\"result\" :result=\"result\">\n\t\t\t\t\t\t\t<div class=\"cn-search-page__result-head\">\n\t\t\t\t\t\t\t\t<span class=\"cn-search-page__result-title\">{{ result.title || result.id }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"result.schema\" class=\"cn-search-page__result-badge\">{{ result.schema }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<p v-if=\"result.snippet\" class=\"cn-search-page__result-snippet\">\n\t\t\t\t\t\t\t\t{{ result.snippet }}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<small v-if=\"result.subtitle\" class=\"cn-search-page__result-subtitle\">{{ result.subtitle }}</small>\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</li>\n\t\t\t\t</ol>\n\t\t\t\t<p v-else-if=\"hasSearched\" class=\"cn-search-page__empty\">\n\t\t\t\t\t{{ emptyLabel }}\n\t\t\t\t</p>\n\t\t\t\t<p v-else-if=\"idleLabel\" class=\"cn-search-page__idle\">\n\t\t\t\t\t{{ idleLabel }}\n\t\t\t\t</p>\n\n\t\t\t\t<div v-if=\"shownTotal > 0 && shownTotal > shownResults.length\" class=\"cn-search-page__more\">\n\t\t\t\t\t{{ totalCountLabel(shownTotal, shownResults.length) }}\n\t\t\t\t</div>\n\t\t\t</section>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { labelLang } from '../../utils/manifestTranslate.js'\n/**\n * CnSearchPage — Search surface with a query input, a facet\n * sidebar, and a results list. Mounted by CnPageRenderer when a\n * manifest page declares `type: 'search'`.\n *\n * The component owns the UI; consumers wire the actual search +\n * facet computation in `@query-change` and `@facets-change` (or use\n * the single `@search` event that fires on both). Cross-schema\n * search, full-text indexing, and facet aggregation are all\n * consumer-side.\n *\n * ```vue\n * <CnSearchPage\n * title=\"Search\"\n * :query=\"q\"\n * :facets=\"facets\"\n * :active-facets=\"activeFacets\"\n * :results=\"results\"\n * :total-count=\"total\"\n * :loading=\"loading\"\n * @search=\"onSearch\" />\n * ```\n */\nexport default {\n\tname: 'CnSearchPage',\n\n\tinject: {\n\t\t/** The label lookup from CnAppRoot, so manifest labels show in the user's language. */\n\t\tcnTranslate: { default: null },\n\t},\n\n\tprops: {\n\t\t/** Optional title rendered above the query input. */\n\t\ttitle: { type: String, default: 'Search' },\n\t\t/**\n\t\t * Controlled query string (v-model style). Falls back to\n\t\t * the local state when omitted; consumers can read the\n\t\t * current value via `@query-change`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tquery: { type: String, default: '' },\n\t\t/** Query input placeholder. */\n\t\tplaceholder: { type: String, default: 'Search…' },\n\t\t/** Query input aria-label. */\n\t\tariaLabel: { type: String, default: 'Search input' },\n\t\t/** Submit-button label. */\n\t\tsearchLabel: { type: String, default: 'Search' },\n\t\t/**\n\t\t * Facet declarations. Each entry: `{ key, label?,\n\t\t * options: [{value, label?, count?}], multiple? }`.\n\t\t * Empty array hides the facet sidebar.\n\t\t *\n\t\t * @type {Array<{key:string,label?:string,options:Array,multiple?:boolean}>}\n\t\t */\n\t\tfacets: { type: Array, default: () => [] },\n\t\t/**\n\t\t * Controlled active-facets map (`{facetKey: [value, ...]}`).\n\t\t * Consumers persist this + push updates back; the widget\n\t\t * emits `@facets-change` with the new map.\n\t\t *\n\t\t * @type {Record<string,Array<string>>}\n\t\t */\n\t\tactiveFacets: { type: Object, default: () => ({}) },\n\t\t/**\n\t\t * Result entries. Each entry rendered through the `#result`\n\t\t * slot or the default `{title, snippet, schema, subtitle}`\n\t\t * shape.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tresults: { type: Array, default: () => [] },\n\t\t/** Total count of matching results (drives the \"more\" footer). */\n\t\ttotalCount: { type: Number, default: 0 },\n\t\t/** Show the loading state. */\n\t\tloading: { type: Boolean, default: false },\n\t\t/** Empty-state when a search ran but matched nothing. */\n\t\temptyLabel: { type: String, default: 'No matches found.' },\n\t\t/** Idle text shown before any search has run. Empty hides. */\n\t\tidleLabel: { type: String, default: 'Start typing to search.' },\n\t\t/** Loading-state text. */\n\t\tloadingLabel: { type: String, default: 'Searching…' },\n\t\t/** Facets sidebar header. */\n\t\tfacetsTitle: { type: String, default: 'Filters' },\n\t\t/** Clear-facets button label. */\n\t\tclearFacetsLabel: { type: String, default: 'Clear filters' },\n\t\t/**\n\t\t * Formatter for the \"Showing X of Y\" footer. Receives total\n\t\t * + shown counts and returns the rendered string.\n\t\t *\n\t\t * @type {(total:number, shown:number) => string}\n\t\t */\n\t\ttotalCountLabel: {\n\t\t\ttype: Function,\n\t\t\tdefault: (total, shown) => `Showing ${shown} of ${total} results.`,\n\t\t},\n\n\t\t/**\n\t\t * The lists the page searches itself (manifest `config.sources`). Each\n\t\t * `{ id?, label, register, schema, route?, titleField?, snippetField?,\n\t\t * limit? }`: one request per list with `_search`, results in the order\n\t\t * of the lists, and the lists offered as a Type filter with their\n\t\t * totals. `route` names the page a result opens, with its id. Empty\n\t\t * keeps the page controlled: `results` and `facets` come from props.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tsources: { type: Array, default: () => [] },\n\t\t/** The Type filter's heading when the page searches `sources`. */\n\t\ttypeLabel: { type: String, default: 'Type' },\n\t},\n\n\temits: ['facets-change', 'query-change', 'result-click', 'search', 'update:query'],\n\tdata() {\n\t\treturn {\n\t\t\tlocalQuery: this.query,\n\t\t\thasSearched: false,\n\t\t\t// What a `sources` search found, per list, and the filter it keeps.\n\t\t\tsourceRows: {},\n\t\t\tsourceTotals: {},\n\t\t\tsourceFacets: {},\n\t\t\tsourceLoading: false,\n\t\t\tsourceRun: 0,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {boolean} Whether the page searches its own `sources`. */\n\t\tsearchesSources() {\n\t\t\treturn Array.isArray(this.sources) && this.sources.some((source) => source && source.register && source.schema)\n\t\t},\n\n\t\t/** @return {Array<object>} The lists searched, each with its key. */\n\t\tsourceList() {\n\t\t\treturn (this.sources || [])\n\t\t\t\t.filter((source) => source && source.register && source.schema)\n\t\t\t\t.map((source) => ({ ...source, key: source.id || source.schema }))\n\t\t},\n\n\t\t/** @return {Record<string, Array<string>>} The active filter, the page's own when it searches `sources`. */\n\t\teffectiveFacets() {\n\t\t\treturn this.searchesSources ? this.sourceFacets : (this.activeFacets || {})\n\t\t},\n\n\t\t/** @return {Array<object>} The facets shown: the Type filter of the lists, else the `facets` prop. */\n\t\tshownFacets() {\n\t\t\tif (!this.searchesSources) {\n\t\t\t\treturn this.facets\n\t\t\t}\n\t\t\tif (!this.hasSearched) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn [{\n\t\t\t\tkey: 'type',\n\t\t\t\tlabel: this.typeLabel,\n\t\t\t\tmultiple: true,\n\t\t\t\toptions: this.sourceList.map((source) => ({\n\t\t\t\t\tvalue: source.key,\n\t\t\t\t\tlabel: source.label,\n\t\t\t\t\t...(typeof this.sourceTotals[source.key] === 'number' ? { count: this.sourceTotals[source.key] } : {}),\n\t\t\t\t})),\n\t\t\t}]\n\t\t},\n\n\t\t/** @return {Array<object>} The results shown. */\n\t\tshownResults() {\n\t\t\tif (!this.searchesSources) {\n\t\t\t\treturn this.results\n\t\t\t}\n\t\t\treturn this.activeSources.flatMap((source) => this.sourceRows[source.key] || [])\n\t\t},\n\n\t\t/** @return {number} The total of the results shown. */\n\t\tshownTotal() {\n\t\t\tif (!this.searchesSources) {\n\t\t\t\treturn this.totalCount\n\t\t\t}\n\t\t\treturn this.activeSources.reduce((sum, source) => sum + (this.sourceTotals[source.key] || 0), 0)\n\t\t},\n\n\t\t/** @return {boolean} Whether a search is running. */\n\t\tshownLoading() {\n\t\t\treturn this.searchesSources ? this.sourceLoading : this.loading\n\t\t},\n\n\t\t/** @return {Array<object>} The lists the Type filter leaves in, all when none is picked. */\n\t\tactiveSources() {\n\t\t\tconst picked = Array.isArray(this.effectiveFacets.type) ? this.effectiveFacets.type : []\n\t\t\treturn picked.length === 0 ? this.sourceList : this.sourceList.filter((source) => picked.includes(source.key))\n\t\t},\n\n\t\t/**\n\t\t * Whether any facet has at least one active value.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasActiveFacets() {\n\t\t\tfor (const k of Object.keys(this.effectiveFacets)) {\n\t\t\t\tif (Array.isArray(this.effectiveFacets[k]) && this.effectiveFacets[k].length > 0) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\t},\n\n\twatch: {\n\t\tquery(next) {\n\t\t\tif (next !== this.localQuery) {\n\t\t\t\tthis.localQuery = next\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest label through the injected lookup, or as written.\n\t\t *\n\t\t * @param {string} text The label as written.\n\t\t * @return {string} The label in the user's language.\n\t\t */\n\t\tshown(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' && typeof text === 'string' && text !== '' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The `lang` of a label that fell back to its written text in another language.\n\t\t *\n\t\t * @param {string} text The label as written.\n\t\t * @return {string|undefined} The source language, or undefined.\n\t\t */\n\t\tlangOf(text) {\n\t\t\treturn labelLang(this.cnTranslate, text) || undefined\n\t\t},\n\n\t\t/**\n\t\t * Whether a (facetKey, value) is currently active.\n\t\t *\n\t\t * @param {string} key Facet key.\n\t\t * @param {string} value Option value.\n\t\t * @return {boolean}\n\t\t */\n\t\tisFacetActive(key, value) {\n\t\t\tconst arr = this.effectiveFacets[key]\n\t\t\treturn Array.isArray(arr) && arr.includes(value)\n\t\t},\n\n\t\t/**\n\t\t * Toggle a facet option on/off and emit the new map.\n\t\t *\n\t\t * @param {string} key Facet key.\n\t\t * @param {string} value Option value.\n\t\t * @param {boolean} checked Checkbox state (radios pass true).\n\t\t * @param {boolean} multiple Whether the facet allows multi-select.\n\t\t * @return {void}\n\t\t */\n\t\ttoggleFacet(key, value, checked, multiple) {\n\t\t\tconst next = { ...this.effectiveFacets }\n\t\t\tif (!multiple) {\n\t\t\t\tnext[key] = checked ? [value] : []\n\t\t\t} else {\n\t\t\t\tconst cur = Array.isArray(next[key]) ? [...next[key]] : []\n\t\t\t\tconst idx = cur.indexOf(value)\n\t\t\t\tif (checked && idx < 0) {\n\t\t\t\t\tcur.push(value)\n\t\t\t\t} else if (!checked && idx >= 0) {\n\t\t\t\t\tcur.splice(idx, 1)\n\t\t\t\t}\n\t\t\t\tnext[key] = cur\n\t\t\t}\n\t\t\tthis.emitFacetsChange(next)\n\t\t},\n\n\t\t/**\n\t\t * Clear every facet.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclearFacets() {\n\t\t\tthis.emitFacetsChange({})\n\t\t},\n\n\t\temitFacetsChange(next) {\n\t\t\t/**\n\t\t\t * @event facets-change Emitted on any facet toggle / clear.\n\t\t\t * @type {Record<string,Array<string>>}\n\t\t\t */\n\t\t\tthis.$emit('facets-change', next)\n\t\t\tif (this.searchesSources) {\n\t\t\t\tthis.sourceFacets = next\n\t\t\t}\n\t\t\tthis.emitSearch()\n\t\t},\n\n\t\t/**\n\t\t * Submit-button handler — propagates the latest query.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonQuerySubmit() {\n\t\t\tthis.emitQueryChange()\n\t\t\tthis.emitSearch()\n\t\t},\n\n\t\t/**\n\t\t * Live query-input handler — emits each keystroke (consumers\n\t\t * are responsible for debouncing).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonQueryInput() {\n\t\t\tthis.emitQueryChange()\n\t\t},\n\n\t\temitQueryChange() {\n\t\t\t/**\n\t\t\t * @event query-change Emitted on every query mutation.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('query-change', this.localQuery)\n\t\t\t/** v-model-friendly alias. */\n\t\t\tthis.$emit('update:query', this.localQuery)\n\t\t},\n\n\t\temitSearch() {\n\t\t\tthis.hasSearched = true\n\t\t\t/**\n\t\t\t * @event search Emitted on Submit or facet change.\n\t\t\t * Combines query + active facets.\n\t\t\t * @type {{ query: string, facets: Record<string,Array<string>> }}\n\t\t\t */\n\t\t\tthis.$emit('search', { query: this.localQuery, facets: { ...this.effectiveFacets } })\n\t\t\tif (this.searchesSources) {\n\t\t\t\tthis.searchSources()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Search the lists the Type filter leaves in, one request each, and\n\t\t * keep what each found. A search started later wins over this one.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync searchSources() {\n\t\t\tconst run = this.sourceRun + 1\n\t\t\tthis.sourceRun = run\n\t\t\tthis.sourceLoading = true\n\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\timport('@nextcloud/router'),\n\t\t\t])\n\t\t\tconst found = await Promise.all(this.activeSources.map(async (source) => {\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: source.register, schema: source.schema })\n\t\t\t\tconst limit = Number(source.limit) > 0 ? Math.floor(Number(source.limit)) : 10\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await axios.get(url, { params: { _search: this.localQuery, _limit: limit } })\n\t\t\t\t\tconst data = response && response.data ? response.data : {}\n\t\t\t\t\tconst rows = Array.isArray(data.results) ? data.results : []\n\t\t\t\t\treturn { source, rows, total: typeof data.total === 'number' ? data.total : rows.length }\n\t\t\t\t} catch (e) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn('[CnSearchPage] could not search', source.schema, e)\n\t\t\t\t\treturn { source, rows: [], total: 0 }\n\t\t\t\t}\n\t\t\t}))\n\t\t\tif (run !== this.sourceRun) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst rows = { ...this.sourceRows }\n\t\t\tconst totals = { ...this.sourceTotals }\n\t\t\tfor (const { source, rows: list, total } of found) {\n\t\t\t\trows[source.key] = list.map((row) => this.resultOf(source, row))\n\t\t\t\ttotals[source.key] = total\n\t\t\t}\n\t\t\tthis.sourceRows = rows\n\t\t\tthis.sourceTotals = totals\n\t\t\tthis.sourceLoading = false\n\t\t},\n\n\t\t/**\n\t\t * One found object as a result row.\n\t\t *\n\t\t * @param {object} source The list it came from.\n\t\t * @param {object} row The object.\n\t\t * @return {object} `{ id, title, snippet, schema, route }`.\n\t\t */\n\t\tresultOf(source, row) {\n\t\t\tconst id = row && (row.id || row['@self']?.id)\n\t\t\tconst titleField = source.titleField || ''\n\t\t\tconst title = (titleField && row[titleField]) || row.title || row.name || id\n\t\t\tconst snippet = source.snippetField ? row[source.snippetField] : undefined\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\ttitle: typeof title === 'string' ? title : String(title ?? ''),\n\t\t\t\t...(typeof snippet === 'string' && snippet !== '' ? { snippet } : {}),\n\t\t\t\tschema: this.shown(source.label) || source.schema,\n\t\t\t\troute: source.route || '',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Forward a result click.\n\t\t *\n\t\t * @param {object} result The clicked result.\n\t\t * @return {void}\n\t\t */\n\t\tonResultClick(result) {\n\t\t\t/**\n\t\t\t * @event result-click Emitted on result-row click.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('result-click', result)\n\t\t\tif (this.searchesSources && result && result.route && result.id && this.$router) {\n\t\t\t\tthis.$router.push({ name: result.route, params: { id: result.id } })\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-search-page {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\tpadding: 16px;\n}\n\n/* Reserve room for NcAppNavigationToggle (matches CnPageRenderer / CnPageHeader). */\n.cn-search-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-search-page__title {\n\tmargin: 0 0 8px;\n\tfont-size: 1.4em;\n}\n\n.cn-search-page__query {\n\tdisplay: flex;\n\tgap: 8px;\n}\n\n.cn-search-page__input {\n\tflex: 1 1 auto;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tfont-size: 1em;\n}\n\n.cn-search-page__submit {\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text, #fff);\n\tborder: none;\n\tpadding: 8px 16px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n}\n\n.cn-search-page__body {\n\tdisplay: grid;\n\tgrid-template-columns: 240px 1fr;\n\tgap: 16px;\n}\n\n.cn-search-page__body:has(:not(.cn-search-page__facets)) {\n\tgrid-template-columns: 1fr;\n}\n\n.cn-search-page__facets {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 12px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-search-page__facets-title {\n\tmargin: 0;\n\tfont-size: 1em;\n}\n\n.cn-search-page__facet-title {\n\tmargin: 0 0 4px;\n\tfont-size: 0.9em;\n}\n\n.cn-search-page__facet-options {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-search-page__facet-option {\n\tdisplay: flex;\n\tgap: 4px;\n}\n\n.cn-search-page__facet-option label {\n\tdisplay: flex;\n\tgap: 6px;\n\talign-items: center;\n\tcursor: pointer;\n\tfont-size: 0.9em;\n}\n\n.cn-search-page__facet-count {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-search-page__clear-facets {\n\tbackground: none;\n\tborder: 1px solid var(--color-border);\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tfont-size: 0.85em;\n\talign-self: flex-start;\n}\n\n.cn-search-page__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-search-page__result {\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tbackground: var(--color-main-background);\n}\n\n.cn-search-page__result:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-search-page__result-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-search-page__result-title {\n\tfont-weight: 600;\n}\n\n.cn-search-page__result-badge {\n\tfont-size: 0.75em;\n\tpadding: 1px 8px;\n\tborder-radius: 9999px;\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\ttext-transform: uppercase;\n\tletter-spacing: 0.05em;\n}\n\n.cn-search-page__result-snippet {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-search-page__result-subtitle {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-search-page__empty,\n.cn-search-page__idle,\n.cn-search-page__loading {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 24px 0;\n\ttext-align: center;\n\tfont-style: italic;\n}\n\n.cn-search-page__more {\n\tmargin-top: 12px;\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n}\n</style>\n"],"names":[],"mappings":"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"}
@@ -94,15 +94,15 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
94
94
  ], 32 /* NEED_HYDRATION */)
95
95
  ]),
96
96
  createElementVNode("div", _hoisted_6, [
97
- ($props.facets.length > 0)
97
+ ($options.shownFacets.length > 0)
98
98
  ? (openBlock(), createElementBlock("aside", _hoisted_7, [
99
99
  createElementVNode("h3", _hoisted_8, toDisplayString($props.facetsTitle), 1 /* TEXT */),
100
- (openBlock(true), createElementBlock(Fragment, null, renderList($props.facets, (facet) => {
100
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.shownFacets, (facet) => {
101
101
  return (openBlock(), createElementBlock("div", {
102
102
  key: facet.key,
103
103
  class: "cn-search-page__facet"
104
104
  }, [
105
- createElementVNode("h4", _hoisted_9, toDisplayString(facet.label || facet.key), 1 /* TEXT */),
105
+ createElementVNode("h4", _hoisted_9, toDisplayString($options.shown(facet.label) || facet.key), 1 /* TEXT */),
106
106
  createElementVNode("ul", _hoisted_10, [
107
107
  (openBlock(true), createElementBlock(Fragment, null, renderList(facet.options, (opt) => {
108
108
  return (openBlock(), createElementBlock("li", {
@@ -116,7 +116,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
116
116
  name: 'facet-' + facet.key,
117
117
  onChange: $event => ($options.toggleFacet(facet.key, opt.value, $event.target.checked, facet.multiple))
118
118
  }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_11),
119
- createElementVNode("span", null, toDisplayString(opt.label || opt.value), 1 /* TEXT */),
119
+ createElementVNode("span", null, toDisplayString($options.shown(opt.label) || opt.value), 1 /* TEXT */),
120
120
  (typeof opt.count === 'number')
121
121
  ? (openBlock(), createElementBlock("small", _hoisted_12, toDisplayString(opt.count), 1 /* TEXT */))
122
122
  : createCommentVNode("v-if", true)
@@ -137,11 +137,11 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
137
137
  ]))
138
138
  : createCommentVNode("v-if", true),
139
139
  createElementVNode("section", _hoisted_13, [
140
- ($props.loading)
140
+ ($options.shownLoading)
141
141
  ? (openBlock(), createElementBlock("div", _hoisted_14, toDisplayString($props.loadingLabel), 1 /* TEXT */))
142
- : ($props.results.length > 0)
142
+ : ($options.shownResults.length > 0)
143
143
  ? (openBlock(), createElementBlock("ol", _hoisted_15, [
144
- (openBlock(true), createElementBlock(Fragment, null, renderList($props.results, (result) => {
144
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.shownResults, (result) => {
145
145
  return (openBlock(), createElementBlock("li", {
146
146
  key: result.id || (result.schema + ':' + (result.title || '')),
147
147
  class: "cn-search-page__result",
@@ -170,8 +170,8 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
170
170
  : ($props.idleLabel)
171
171
  ? (openBlock(), createElementBlock("p", _hoisted_23, toDisplayString($props.idleLabel), 1 /* TEXT */))
172
172
  : createCommentVNode("v-if", true),
173
- ($props.totalCount > 0 && $props.totalCount > $props.results.length)
174
- ? (openBlock(), createElementBlock("div", _hoisted_24, toDisplayString($props.totalCountLabel($props.totalCount, $props.results.length)), 1 /* TEXT */))
173
+ ($options.shownTotal > 0 && $options.shownTotal > $options.shownResults.length)
174
+ ? (openBlock(), createElementBlock("div", _hoisted_24, toDisplayString($props.totalCountLabel($options.shownTotal, $options.shownResults.length)), 1 /* TEXT */))
175
175
  : createCommentVNode("v-if", true)
176
176
  ])
177
177
  ])
@@ -1 +1 @@
1
- {"version":3,"file":"CnSearchPage.vue3.js","sources":["../../../../src/components/CnSearchPage/CnSearchPage.vue?vue&type=template&id=2456d0f2&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-search-page\" data-testid=\"cn-search-page\">\n\t\t<header class=\"cn-search-page__header\">\n\t\t\t<h2 v-if=\"title\" class=\"cn-search-page__title\" :lang=\"langOf(title)\">\n\t\t\t\t{{ shown(title) }}\n\t\t\t</h2>\n\t\t\t<form class=\"cn-search-page__query\" @submit.prevent=\"onQuerySubmit\">\n\t\t\t\t<input\n\t\t\t\t\tv-model=\"localQuery\"\n\t\t\t\t\ttype=\"search\"\n\t\t\t\t\t:placeholder=\"placeholder\"\n\t\t\t\t\t:aria-label=\"ariaLabel\"\n\t\t\t\t\tclass=\"cn-search-page__input\"\n\t\t\t\t\tdata-testid=\"cn-search-page-input\"\n\t\t\t\t\t@input=\"onQueryInput\">\n\t\t\t\t<button type=\"submit\" class=\"cn-search-page__submit\">\n\t\t\t\t\t{{ searchLabel }}\n\t\t\t\t</button>\n\t\t\t</form>\n\t\t</header>\n\n\t\t<div class=\"cn-search-page__body\">\n\t\t\t<!-- Facet sidebar. -->\n\t\t\t<aside v-if=\"facets.length > 0\" class=\"cn-search-page__facets\" data-testid=\"cn-search-page-facets\">\n\t\t\t\t<h3 class=\"cn-search-page__facets-title\">\n\t\t\t\t\t{{ facetsTitle }}\n\t\t\t\t</h3>\n\t\t\t\t<div v-for=\"facet in facets\" :key=\"facet.key\" class=\"cn-search-page__facet\">\n\t\t\t\t\t<h4 class=\"cn-search-page__facet-title\">\n\t\t\t\t\t\t{{ facet.label || facet.key }}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<ul class=\"cn-search-page__facet-options\">\n\t\t\t\t\t\t<li v-for=\"opt in facet.options\"\n\t\t\t\t\t\t\t:key=\"opt.value\"\n\t\t\t\t\t\t\tclass=\"cn-search-page__facet-option\">\n\t\t\t\t\t\t\t<label>\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t:model-value=\"isFacetActive(facet.key, opt.value)\"\n\t\t\t\t\t\t\t\t\t:type=\"facet.multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t\t\t\t:name=\"'facet-' + facet.key\"\n\t\t\t\t\t\t\t\t\t@change=\"toggleFacet(facet.key, opt.value, $event.target.checked, facet.multiple)\">\n\t\t\t\t\t\t\t\t<span>{{ opt.label || opt.value }}</span>\n\t\t\t\t\t\t\t\t<small v-if=\"typeof opt.count === 'number'\" class=\"cn-search-page__facet-count\">{{ opt.count }}</small>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</div>\n\t\t\t\t<button v-if=\"hasActiveFacets\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-search-page__clear-facets\"\n\t\t\t\t\t@click=\"clearFacets\">\n\t\t\t\t\t{{ clearFacetsLabel }}\n\t\t\t\t</button>\n\t\t\t</aside>\n\n\t\t\t<!-- Results pane. -->\n\t\t\t<section class=\"cn-search-page__results\" data-testid=\"cn-search-page-results\">\n\t\t\t\t<div v-if=\"loading\" class=\"cn-search-page__loading\">\n\t\t\t\t\t{{ loadingLabel }}\n\t\t\t\t</div>\n\t\t\t\t<ol v-else-if=\"results.length > 0\" class=\"cn-search-page__list\">\n\t\t\t\t\t<li v-for=\"result in results\"\n\t\t\t\t\t\t:key=\"result.id || (result.schema + ':' + (result.title || ''))\"\n\t\t\t\t\t\tclass=\"cn-search-page__result\"\n\t\t\t\t\t\t:data-result-id=\"result.id\"\n\t\t\t\t\t\t@click=\"onResultClick(result)\">\n\t\t\t\t\t\t<!-- @slot result Per-result body. Scope:\n\t\t\t\t\t\t { result }. Default renders title +\n\t\t\t\t\t\t snippet + schema badge. -->\n\t\t\t\t\t\t<slot name=\"result\" :result=\"result\">\n\t\t\t\t\t\t\t<div class=\"cn-search-page__result-head\">\n\t\t\t\t\t\t\t\t<span class=\"cn-search-page__result-title\">{{ result.title || result.id }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"result.schema\" class=\"cn-search-page__result-badge\">{{ result.schema }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<p v-if=\"result.snippet\" class=\"cn-search-page__result-snippet\">\n\t\t\t\t\t\t\t\t{{ result.snippet }}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<small v-if=\"result.subtitle\" class=\"cn-search-page__result-subtitle\">{{ result.subtitle }}</small>\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</li>\n\t\t\t\t</ol>\n\t\t\t\t<p v-else-if=\"hasSearched\" class=\"cn-search-page__empty\">\n\t\t\t\t\t{{ emptyLabel }}\n\t\t\t\t</p>\n\t\t\t\t<p v-else-if=\"idleLabel\" class=\"cn-search-page__idle\">\n\t\t\t\t\t{{ idleLabel }}\n\t\t\t\t</p>\n\n\t\t\t\t<div v-if=\"totalCount > 0 && totalCount > results.length\" class=\"cn-search-page__more\">\n\t\t\t\t\t{{ totalCountLabel(totalCount, results.length) }}\n\t\t\t\t</div>\n\t\t\t</section>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { labelLang } from '../../utils/manifestTranslate.js'\n/**\n * CnSearchPage — Search surface with a query input, a facet\n * sidebar, and a results list. Mounted by CnPageRenderer when a\n * manifest page declares `type: 'search'`.\n *\n * The component owns the UI; consumers wire the actual search +\n * facet computation in `@query-change` and `@facets-change` (or use\n * the single `@search` event that fires on both). Cross-schema\n * search, full-text indexing, and facet aggregation are all\n * consumer-side.\n *\n * ```vue\n * <CnSearchPage\n * title=\"Search\"\n * :query=\"q\"\n * :facets=\"facets\"\n * :active-facets=\"activeFacets\"\n * :results=\"results\"\n * :total-count=\"total\"\n * :loading=\"loading\"\n * @search=\"onSearch\" />\n * ```\n */\nexport default {\n\tname: 'CnSearchPage',\n\n\tinject: {\n\t\t/** The label lookup from CnAppRoot, so manifest labels show in the user's language. */\n\t\tcnTranslate: { default: null },\n\t},\n\n\tprops: {\n\t\t/** Optional title rendered above the query input. */\n\t\ttitle: { type: String, default: 'Search' },\n\t\t/**\n\t\t * Controlled query string (v-model style). Falls back to\n\t\t * the local state when omitted; consumers can read the\n\t\t * current value via `@query-change`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tquery: { type: String, default: '' },\n\t\t/** Query input placeholder. */\n\t\tplaceholder: { type: String, default: 'Search…' },\n\t\t/** Query input aria-label. */\n\t\tariaLabel: { type: String, default: 'Search input' },\n\t\t/** Submit-button label. */\n\t\tsearchLabel: { type: String, default: 'Search' },\n\t\t/**\n\t\t * Facet declarations. Each entry: `{ key, label?,\n\t\t * options: [{value, label?, count?}], multiple? }`.\n\t\t * Empty array hides the facet sidebar.\n\t\t *\n\t\t * @type {Array<{key:string,label?:string,options:Array,multiple?:boolean}>}\n\t\t */\n\t\tfacets: { type: Array, default: () => [] },\n\t\t/**\n\t\t * Controlled active-facets map (`{facetKey: [value, ...]}`).\n\t\t * Consumers persist this + push updates back; the widget\n\t\t * emits `@facets-change` with the new map.\n\t\t *\n\t\t * @type {Record<string,Array<string>>}\n\t\t */\n\t\tactiveFacets: { type: Object, default: () => ({}) },\n\t\t/**\n\t\t * Result entries. Each entry rendered through the `#result`\n\t\t * slot or the default `{title, snippet, schema, subtitle}`\n\t\t * shape.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tresults: { type: Array, default: () => [] },\n\t\t/** Total count of matching results (drives the \"more\" footer). */\n\t\ttotalCount: { type: Number, default: 0 },\n\t\t/** Show the loading state. */\n\t\tloading: { type: Boolean, default: false },\n\t\t/** Empty-state when a search ran but matched nothing. */\n\t\temptyLabel: { type: String, default: 'No matches found.' },\n\t\t/** Idle text shown before any search has run. Empty hides. */\n\t\tidleLabel: { type: String, default: 'Start typing to search.' },\n\t\t/** Loading-state text. */\n\t\tloadingLabel: { type: String, default: 'Searching…' },\n\t\t/** Facets sidebar header. */\n\t\tfacetsTitle: { type: String, default: 'Filters' },\n\t\t/** Clear-facets button label. */\n\t\tclearFacetsLabel: { type: String, default: 'Clear filters' },\n\t\t/**\n\t\t * Formatter for the \"Showing X of Y\" footer. Receives total\n\t\t * + shown counts and returns the rendered string.\n\t\t *\n\t\t * @type {(total:number, shown:number) => string}\n\t\t */\n\t\ttotalCountLabel: {\n\t\t\ttype: Function,\n\t\t\tdefault: (total, shown) => `Showing ${shown} of ${total} results.`,\n\t\t},\n\t},\n\n\temits: ['facets-change', 'query-change', 'result-click', 'search', 'update:query'],\n\tdata() {\n\t\treturn {\n\t\t\tlocalQuery: this.query,\n\t\t\thasSearched: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether any facet has at least one active value.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasActiveFacets() {\n\t\t\tfor (const k of Object.keys(this.activeFacets || {})) {\n\t\t\t\tif (Array.isArray(this.activeFacets[k]) && this.activeFacets[k].length > 0) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\t},\n\n\twatch: {\n\t\tquery(next) {\n\t\t\tif (next !== this.localQuery) {\n\t\t\t\tthis.localQuery = next\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest label through the injected lookup, or as written.\n\t\t *\n\t\t * @param {string} text The label as written.\n\t\t * @return {string} The label in the user's language.\n\t\t */\n\t\tshown(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' && typeof text === 'string' && text !== '' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The `lang` of a label that fell back to its written text in another language.\n\t\t *\n\t\t * @param {string} text The label as written.\n\t\t * @return {string|undefined} The source language, or undefined.\n\t\t */\n\t\tlangOf(text) {\n\t\t\treturn labelLang(this.cnTranslate, text) || undefined\n\t\t},\n\n\t\t/**\n\t\t * Whether a (facetKey, value) is currently active.\n\t\t *\n\t\t * @param {string} key Facet key.\n\t\t * @param {string} value Option value.\n\t\t * @return {boolean}\n\t\t */\n\t\tisFacetActive(key, value) {\n\t\t\tconst arr = this.activeFacets[key]\n\t\t\treturn Array.isArray(arr) && arr.includes(value)\n\t\t},\n\n\t\t/**\n\t\t * Toggle a facet option on/off and emit the new map.\n\t\t *\n\t\t * @param {string} key Facet key.\n\t\t * @param {string} value Option value.\n\t\t * @param {boolean} checked Checkbox state (radios pass true).\n\t\t * @param {boolean} multiple Whether the facet allows multi-select.\n\t\t * @return {void}\n\t\t */\n\t\ttoggleFacet(key, value, checked, multiple) {\n\t\t\tconst next = { ...this.activeFacets }\n\t\t\tif (!multiple) {\n\t\t\t\tnext[key] = checked ? [value] : []\n\t\t\t} else {\n\t\t\t\tconst cur = Array.isArray(next[key]) ? [...next[key]] : []\n\t\t\t\tconst idx = cur.indexOf(value)\n\t\t\t\tif (checked && idx < 0) {\n\t\t\t\t\tcur.push(value)\n\t\t\t\t} else if (!checked && idx >= 0) {\n\t\t\t\t\tcur.splice(idx, 1)\n\t\t\t\t}\n\t\t\t\tnext[key] = cur\n\t\t\t}\n\t\t\tthis.emitFacetsChange(next)\n\t\t},\n\n\t\t/**\n\t\t * Clear every facet.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclearFacets() {\n\t\t\tthis.emitFacetsChange({})\n\t\t},\n\n\t\temitFacetsChange(next) {\n\t\t\t/**\n\t\t\t * @event facets-change Emitted on any facet toggle / clear.\n\t\t\t * @type {Record<string,Array<string>>}\n\t\t\t */\n\t\t\tthis.$emit('facets-change', next)\n\t\t\tthis.emitSearch()\n\t\t},\n\n\t\t/**\n\t\t * Submit-button handler — propagates the latest query.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonQuerySubmit() {\n\t\t\tthis.emitQueryChange()\n\t\t\tthis.emitSearch()\n\t\t},\n\n\t\t/**\n\t\t * Live query-input handler — emits each keystroke (consumers\n\t\t * are responsible for debouncing).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonQueryInput() {\n\t\t\tthis.emitQueryChange()\n\t\t},\n\n\t\temitQueryChange() {\n\t\t\t/**\n\t\t\t * @event query-change Emitted on every query mutation.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('query-change', this.localQuery)\n\t\t\t/** v-model-friendly alias. */\n\t\t\tthis.$emit('update:query', this.localQuery)\n\t\t},\n\n\t\temitSearch() {\n\t\t\tthis.hasSearched = true\n\t\t\t/**\n\t\t\t * @event search Emitted on Submit or facet change.\n\t\t\t * Combines query + active facets.\n\t\t\t * @type {{ query: string, facets: Record<string,Array<string>> }}\n\t\t\t */\n\t\t\tthis.$emit('search', { query: this.localQuery, facets: { ...this.activeFacets } })\n\t\t},\n\n\t\t/**\n\t\t * Forward a result click.\n\t\t *\n\t\t * @param {object} result The clicked result.\n\t\t * @return {void}\n\t\t */\n\t\tonResultClick(result) {\n\t\t\t/**\n\t\t\t * @event result-click Emitted on result-row click.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('result-click', result)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-search-page {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\tpadding: 16px;\n}\n\n/* Reserve room for NcAppNavigationToggle (matches CnPageRenderer / CnPageHeader). */\n.cn-search-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-search-page__title {\n\tmargin: 0 0 8px;\n\tfont-size: 1.4em;\n}\n\n.cn-search-page__query {\n\tdisplay: flex;\n\tgap: 8px;\n}\n\n.cn-search-page__input {\n\tflex: 1 1 auto;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tfont-size: 1em;\n}\n\n.cn-search-page__submit {\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text, #fff);\n\tborder: none;\n\tpadding: 8px 16px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n}\n\n.cn-search-page__body {\n\tdisplay: grid;\n\tgrid-template-columns: 240px 1fr;\n\tgap: 16px;\n}\n\n.cn-search-page__body:has(:not(.cn-search-page__facets)) {\n\tgrid-template-columns: 1fr;\n}\n\n.cn-search-page__facets {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 12px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-search-page__facets-title {\n\tmargin: 0;\n\tfont-size: 1em;\n}\n\n.cn-search-page__facet-title {\n\tmargin: 0 0 4px;\n\tfont-size: 0.9em;\n}\n\n.cn-search-page__facet-options {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-search-page__facet-option {\n\tdisplay: flex;\n\tgap: 4px;\n}\n\n.cn-search-page__facet-option label {\n\tdisplay: flex;\n\tgap: 6px;\n\talign-items: center;\n\tcursor: pointer;\n\tfont-size: 0.9em;\n}\n\n.cn-search-page__facet-count {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-search-page__clear-facets {\n\tbackground: none;\n\tborder: 1px solid var(--color-border);\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tfont-size: 0.85em;\n\talign-self: flex-start;\n}\n\n.cn-search-page__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-search-page__result {\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tbackground: var(--color-main-background);\n}\n\n.cn-search-page__result:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-search-page__result-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-search-page__result-title {\n\tfont-weight: 600;\n}\n\n.cn-search-page__result-badge {\n\tfont-size: 0.75em;\n\tpadding: 1px 8px;\n\tborder-radius: 9999px;\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\ttext-transform: uppercase;\n\tletter-spacing: 0.05em;\n}\n\n.cn-search-page__result-snippet {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-search-page__result-subtitle {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-search-page__empty,\n.cn-search-page__idle,\n.cn-search-page__loading {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 24px 0;\n\ttext-align: center;\n\tfont-style: italic;\n}\n\n.cn-search-page__more {\n\tmargin-top: 12px;\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createElementVNode","_toDisplayString","_Fragment","_renderList","_renderSlot"],"mappings":"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"}
1
+ {"version":3,"file":"CnSearchPage.vue3.js","sources":["../../../../src/components/CnSearchPage/CnSearchPage.vue?vue&type=template&id=ae9b2ed8&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-search-page\" data-testid=\"cn-search-page\">\n\t\t<header class=\"cn-search-page__header\">\n\t\t\t<h2 v-if=\"title\" class=\"cn-search-page__title\" :lang=\"langOf(title)\">\n\t\t\t\t{{ shown(title) }}\n\t\t\t</h2>\n\t\t\t<form class=\"cn-search-page__query\" @submit.prevent=\"onQuerySubmit\">\n\t\t\t\t<input\n\t\t\t\t\tv-model=\"localQuery\"\n\t\t\t\t\ttype=\"search\"\n\t\t\t\t\t:placeholder=\"placeholder\"\n\t\t\t\t\t:aria-label=\"ariaLabel\"\n\t\t\t\t\tclass=\"cn-search-page__input\"\n\t\t\t\t\tdata-testid=\"cn-search-page-input\"\n\t\t\t\t\t@input=\"onQueryInput\">\n\t\t\t\t<button type=\"submit\" class=\"cn-search-page__submit\">\n\t\t\t\t\t{{ searchLabel }}\n\t\t\t\t</button>\n\t\t\t</form>\n\t\t</header>\n\n\t\t<div class=\"cn-search-page__body\">\n\t\t\t<!-- Facet sidebar. -->\n\t\t\t<aside v-if=\"shownFacets.length > 0\" class=\"cn-search-page__facets\" data-testid=\"cn-search-page-facets\">\n\t\t\t\t<h3 class=\"cn-search-page__facets-title\">\n\t\t\t\t\t{{ facetsTitle }}\n\t\t\t\t</h3>\n\t\t\t\t<div v-for=\"facet in shownFacets\" :key=\"facet.key\" class=\"cn-search-page__facet\">\n\t\t\t\t\t<h4 class=\"cn-search-page__facet-title\">\n\t\t\t\t\t\t{{ shown(facet.label) || facet.key }}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<ul class=\"cn-search-page__facet-options\">\n\t\t\t\t\t\t<li v-for=\"opt in facet.options\"\n\t\t\t\t\t\t\t:key=\"opt.value\"\n\t\t\t\t\t\t\tclass=\"cn-search-page__facet-option\">\n\t\t\t\t\t\t\t<label>\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t:model-value=\"isFacetActive(facet.key, opt.value)\"\n\t\t\t\t\t\t\t\t\t:type=\"facet.multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t\t\t\t:name=\"'facet-' + facet.key\"\n\t\t\t\t\t\t\t\t\t@change=\"toggleFacet(facet.key, opt.value, $event.target.checked, facet.multiple)\">\n\t\t\t\t\t\t\t\t<span>{{ shown(opt.label) || opt.value }}</span>\n\t\t\t\t\t\t\t\t<small v-if=\"typeof opt.count === 'number'\" class=\"cn-search-page__facet-count\">{{ opt.count }}</small>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</div>\n\t\t\t\t<button v-if=\"hasActiveFacets\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-search-page__clear-facets\"\n\t\t\t\t\t@click=\"clearFacets\">\n\t\t\t\t\t{{ clearFacetsLabel }}\n\t\t\t\t</button>\n\t\t\t</aside>\n\n\t\t\t<!-- Results pane. -->\n\t\t\t<section class=\"cn-search-page__results\" data-testid=\"cn-search-page-results\">\n\t\t\t\t<div v-if=\"shownLoading\" class=\"cn-search-page__loading\">\n\t\t\t\t\t{{ loadingLabel }}\n\t\t\t\t</div>\n\t\t\t\t<ol v-else-if=\"shownResults.length > 0\" class=\"cn-search-page__list\">\n\t\t\t\t\t<li v-for=\"result in shownResults\"\n\t\t\t\t\t\t:key=\"result.id || (result.schema + ':' + (result.title || ''))\"\n\t\t\t\t\t\tclass=\"cn-search-page__result\"\n\t\t\t\t\t\t:data-result-id=\"result.id\"\n\t\t\t\t\t\t@click=\"onResultClick(result)\">\n\t\t\t\t\t\t<!-- @slot result Per-result body. Scope:\n\t\t\t\t\t\t { result }. Default renders title +\n\t\t\t\t\t\t snippet + schema badge. -->\n\t\t\t\t\t\t<slot name=\"result\" :result=\"result\">\n\t\t\t\t\t\t\t<div class=\"cn-search-page__result-head\">\n\t\t\t\t\t\t\t\t<span class=\"cn-search-page__result-title\">{{ result.title || result.id }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"result.schema\" class=\"cn-search-page__result-badge\">{{ result.schema }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<p v-if=\"result.snippet\" class=\"cn-search-page__result-snippet\">\n\t\t\t\t\t\t\t\t{{ result.snippet }}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<small v-if=\"result.subtitle\" class=\"cn-search-page__result-subtitle\">{{ result.subtitle }}</small>\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</li>\n\t\t\t\t</ol>\n\t\t\t\t<p v-else-if=\"hasSearched\" class=\"cn-search-page__empty\">\n\t\t\t\t\t{{ emptyLabel }}\n\t\t\t\t</p>\n\t\t\t\t<p v-else-if=\"idleLabel\" class=\"cn-search-page__idle\">\n\t\t\t\t\t{{ idleLabel }}\n\t\t\t\t</p>\n\n\t\t\t\t<div v-if=\"shownTotal > 0 && shownTotal > shownResults.length\" class=\"cn-search-page__more\">\n\t\t\t\t\t{{ totalCountLabel(shownTotal, shownResults.length) }}\n\t\t\t\t</div>\n\t\t\t</section>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { labelLang } from '../../utils/manifestTranslate.js'\n/**\n * CnSearchPage — Search surface with a query input, a facet\n * sidebar, and a results list. Mounted by CnPageRenderer when a\n * manifest page declares `type: 'search'`.\n *\n * The component owns the UI; consumers wire the actual search +\n * facet computation in `@query-change` and `@facets-change` (or use\n * the single `@search` event that fires on both). Cross-schema\n * search, full-text indexing, and facet aggregation are all\n * consumer-side.\n *\n * ```vue\n * <CnSearchPage\n * title=\"Search\"\n * :query=\"q\"\n * :facets=\"facets\"\n * :active-facets=\"activeFacets\"\n * :results=\"results\"\n * :total-count=\"total\"\n * :loading=\"loading\"\n * @search=\"onSearch\" />\n * ```\n */\nexport default {\n\tname: 'CnSearchPage',\n\n\tinject: {\n\t\t/** The label lookup from CnAppRoot, so manifest labels show in the user's language. */\n\t\tcnTranslate: { default: null },\n\t},\n\n\tprops: {\n\t\t/** Optional title rendered above the query input. */\n\t\ttitle: { type: String, default: 'Search' },\n\t\t/**\n\t\t * Controlled query string (v-model style). Falls back to\n\t\t * the local state when omitted; consumers can read the\n\t\t * current value via `@query-change`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tquery: { type: String, default: '' },\n\t\t/** Query input placeholder. */\n\t\tplaceholder: { type: String, default: 'Search…' },\n\t\t/** Query input aria-label. */\n\t\tariaLabel: { type: String, default: 'Search input' },\n\t\t/** Submit-button label. */\n\t\tsearchLabel: { type: String, default: 'Search' },\n\t\t/**\n\t\t * Facet declarations. Each entry: `{ key, label?,\n\t\t * options: [{value, label?, count?}], multiple? }`.\n\t\t * Empty array hides the facet sidebar.\n\t\t *\n\t\t * @type {Array<{key:string,label?:string,options:Array,multiple?:boolean}>}\n\t\t */\n\t\tfacets: { type: Array, default: () => [] },\n\t\t/**\n\t\t * Controlled active-facets map (`{facetKey: [value, ...]}`).\n\t\t * Consumers persist this + push updates back; the widget\n\t\t * emits `@facets-change` with the new map.\n\t\t *\n\t\t * @type {Record<string,Array<string>>}\n\t\t */\n\t\tactiveFacets: { type: Object, default: () => ({}) },\n\t\t/**\n\t\t * Result entries. Each entry rendered through the `#result`\n\t\t * slot or the default `{title, snippet, schema, subtitle}`\n\t\t * shape.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tresults: { type: Array, default: () => [] },\n\t\t/** Total count of matching results (drives the \"more\" footer). */\n\t\ttotalCount: { type: Number, default: 0 },\n\t\t/** Show the loading state. */\n\t\tloading: { type: Boolean, default: false },\n\t\t/** Empty-state when a search ran but matched nothing. */\n\t\temptyLabel: { type: String, default: 'No matches found.' },\n\t\t/** Idle text shown before any search has run. Empty hides. */\n\t\tidleLabel: { type: String, default: 'Start typing to search.' },\n\t\t/** Loading-state text. */\n\t\tloadingLabel: { type: String, default: 'Searching…' },\n\t\t/** Facets sidebar header. */\n\t\tfacetsTitle: { type: String, default: 'Filters' },\n\t\t/** Clear-facets button label. */\n\t\tclearFacetsLabel: { type: String, default: 'Clear filters' },\n\t\t/**\n\t\t * Formatter for the \"Showing X of Y\" footer. Receives total\n\t\t * + shown counts and returns the rendered string.\n\t\t *\n\t\t * @type {(total:number, shown:number) => string}\n\t\t */\n\t\ttotalCountLabel: {\n\t\t\ttype: Function,\n\t\t\tdefault: (total, shown) => `Showing ${shown} of ${total} results.`,\n\t\t},\n\n\t\t/**\n\t\t * The lists the page searches itself (manifest `config.sources`). Each\n\t\t * `{ id?, label, register, schema, route?, titleField?, snippetField?,\n\t\t * limit? }`: one request per list with `_search`, results in the order\n\t\t * of the lists, and the lists offered as a Type filter with their\n\t\t * totals. `route` names the page a result opens, with its id. Empty\n\t\t * keeps the page controlled: `results` and `facets` come from props.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tsources: { type: Array, default: () => [] },\n\t\t/** The Type filter's heading when the page searches `sources`. */\n\t\ttypeLabel: { type: String, default: 'Type' },\n\t},\n\n\temits: ['facets-change', 'query-change', 'result-click', 'search', 'update:query'],\n\tdata() {\n\t\treturn {\n\t\t\tlocalQuery: this.query,\n\t\t\thasSearched: false,\n\t\t\t// What a `sources` search found, per list, and the filter it keeps.\n\t\t\tsourceRows: {},\n\t\t\tsourceTotals: {},\n\t\t\tsourceFacets: {},\n\t\t\tsourceLoading: false,\n\t\t\tsourceRun: 0,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {boolean} Whether the page searches its own `sources`. */\n\t\tsearchesSources() {\n\t\t\treturn Array.isArray(this.sources) && this.sources.some((source) => source && source.register && source.schema)\n\t\t},\n\n\t\t/** @return {Array<object>} The lists searched, each with its key. */\n\t\tsourceList() {\n\t\t\treturn (this.sources || [])\n\t\t\t\t.filter((source) => source && source.register && source.schema)\n\t\t\t\t.map((source) => ({ ...source, key: source.id || source.schema }))\n\t\t},\n\n\t\t/** @return {Record<string, Array<string>>} The active filter, the page's own when it searches `sources`. */\n\t\teffectiveFacets() {\n\t\t\treturn this.searchesSources ? this.sourceFacets : (this.activeFacets || {})\n\t\t},\n\n\t\t/** @return {Array<object>} The facets shown: the Type filter of the lists, else the `facets` prop. */\n\t\tshownFacets() {\n\t\t\tif (!this.searchesSources) {\n\t\t\t\treturn this.facets\n\t\t\t}\n\t\t\tif (!this.hasSearched) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn [{\n\t\t\t\tkey: 'type',\n\t\t\t\tlabel: this.typeLabel,\n\t\t\t\tmultiple: true,\n\t\t\t\toptions: this.sourceList.map((source) => ({\n\t\t\t\t\tvalue: source.key,\n\t\t\t\t\tlabel: source.label,\n\t\t\t\t\t...(typeof this.sourceTotals[source.key] === 'number' ? { count: this.sourceTotals[source.key] } : {}),\n\t\t\t\t})),\n\t\t\t}]\n\t\t},\n\n\t\t/** @return {Array<object>} The results shown. */\n\t\tshownResults() {\n\t\t\tif (!this.searchesSources) {\n\t\t\t\treturn this.results\n\t\t\t}\n\t\t\treturn this.activeSources.flatMap((source) => this.sourceRows[source.key] || [])\n\t\t},\n\n\t\t/** @return {number} The total of the results shown. */\n\t\tshownTotal() {\n\t\t\tif (!this.searchesSources) {\n\t\t\t\treturn this.totalCount\n\t\t\t}\n\t\t\treturn this.activeSources.reduce((sum, source) => sum + (this.sourceTotals[source.key] || 0), 0)\n\t\t},\n\n\t\t/** @return {boolean} Whether a search is running. */\n\t\tshownLoading() {\n\t\t\treturn this.searchesSources ? this.sourceLoading : this.loading\n\t\t},\n\n\t\t/** @return {Array<object>} The lists the Type filter leaves in, all when none is picked. */\n\t\tactiveSources() {\n\t\t\tconst picked = Array.isArray(this.effectiveFacets.type) ? this.effectiveFacets.type : []\n\t\t\treturn picked.length === 0 ? this.sourceList : this.sourceList.filter((source) => picked.includes(source.key))\n\t\t},\n\n\t\t/**\n\t\t * Whether any facet has at least one active value.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasActiveFacets() {\n\t\t\tfor (const k of Object.keys(this.effectiveFacets)) {\n\t\t\t\tif (Array.isArray(this.effectiveFacets[k]) && this.effectiveFacets[k].length > 0) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\t},\n\n\twatch: {\n\t\tquery(next) {\n\t\t\tif (next !== this.localQuery) {\n\t\t\t\tthis.localQuery = next\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest label through the injected lookup, or as written.\n\t\t *\n\t\t * @param {string} text The label as written.\n\t\t * @return {string} The label in the user's language.\n\t\t */\n\t\tshown(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' && typeof text === 'string' && text !== '' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The `lang` of a label that fell back to its written text in another language.\n\t\t *\n\t\t * @param {string} text The label as written.\n\t\t * @return {string|undefined} The source language, or undefined.\n\t\t */\n\t\tlangOf(text) {\n\t\t\treturn labelLang(this.cnTranslate, text) || undefined\n\t\t},\n\n\t\t/**\n\t\t * Whether a (facetKey, value) is currently active.\n\t\t *\n\t\t * @param {string} key Facet key.\n\t\t * @param {string} value Option value.\n\t\t * @return {boolean}\n\t\t */\n\t\tisFacetActive(key, value) {\n\t\t\tconst arr = this.effectiveFacets[key]\n\t\t\treturn Array.isArray(arr) && arr.includes(value)\n\t\t},\n\n\t\t/**\n\t\t * Toggle a facet option on/off and emit the new map.\n\t\t *\n\t\t * @param {string} key Facet key.\n\t\t * @param {string} value Option value.\n\t\t * @param {boolean} checked Checkbox state (radios pass true).\n\t\t * @param {boolean} multiple Whether the facet allows multi-select.\n\t\t * @return {void}\n\t\t */\n\t\ttoggleFacet(key, value, checked, multiple) {\n\t\t\tconst next = { ...this.effectiveFacets }\n\t\t\tif (!multiple) {\n\t\t\t\tnext[key] = checked ? [value] : []\n\t\t\t} else {\n\t\t\t\tconst cur = Array.isArray(next[key]) ? [...next[key]] : []\n\t\t\t\tconst idx = cur.indexOf(value)\n\t\t\t\tif (checked && idx < 0) {\n\t\t\t\t\tcur.push(value)\n\t\t\t\t} else if (!checked && idx >= 0) {\n\t\t\t\t\tcur.splice(idx, 1)\n\t\t\t\t}\n\t\t\t\tnext[key] = cur\n\t\t\t}\n\t\t\tthis.emitFacetsChange(next)\n\t\t},\n\n\t\t/**\n\t\t * Clear every facet.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclearFacets() {\n\t\t\tthis.emitFacetsChange({})\n\t\t},\n\n\t\temitFacetsChange(next) {\n\t\t\t/**\n\t\t\t * @event facets-change Emitted on any facet toggle / clear.\n\t\t\t * @type {Record<string,Array<string>>}\n\t\t\t */\n\t\t\tthis.$emit('facets-change', next)\n\t\t\tif (this.searchesSources) {\n\t\t\t\tthis.sourceFacets = next\n\t\t\t}\n\t\t\tthis.emitSearch()\n\t\t},\n\n\t\t/**\n\t\t * Submit-button handler — propagates the latest query.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonQuerySubmit() {\n\t\t\tthis.emitQueryChange()\n\t\t\tthis.emitSearch()\n\t\t},\n\n\t\t/**\n\t\t * Live query-input handler — emits each keystroke (consumers\n\t\t * are responsible for debouncing).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonQueryInput() {\n\t\t\tthis.emitQueryChange()\n\t\t},\n\n\t\temitQueryChange() {\n\t\t\t/**\n\t\t\t * @event query-change Emitted on every query mutation.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('query-change', this.localQuery)\n\t\t\t/** v-model-friendly alias. */\n\t\t\tthis.$emit('update:query', this.localQuery)\n\t\t},\n\n\t\temitSearch() {\n\t\t\tthis.hasSearched = true\n\t\t\t/**\n\t\t\t * @event search Emitted on Submit or facet change.\n\t\t\t * Combines query + active facets.\n\t\t\t * @type {{ query: string, facets: Record<string,Array<string>> }}\n\t\t\t */\n\t\t\tthis.$emit('search', { query: this.localQuery, facets: { ...this.effectiveFacets } })\n\t\t\tif (this.searchesSources) {\n\t\t\t\tthis.searchSources()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Search the lists the Type filter leaves in, one request each, and\n\t\t * keep what each found. A search started later wins over this one.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync searchSources() {\n\t\t\tconst run = this.sourceRun + 1\n\t\t\tthis.sourceRun = run\n\t\t\tthis.sourceLoading = true\n\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\timport('@nextcloud/router'),\n\t\t\t])\n\t\t\tconst found = await Promise.all(this.activeSources.map(async (source) => {\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: source.register, schema: source.schema })\n\t\t\t\tconst limit = Number(source.limit) > 0 ? Math.floor(Number(source.limit)) : 10\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await axios.get(url, { params: { _search: this.localQuery, _limit: limit } })\n\t\t\t\t\tconst data = response && response.data ? response.data : {}\n\t\t\t\t\tconst rows = Array.isArray(data.results) ? data.results : []\n\t\t\t\t\treturn { source, rows, total: typeof data.total === 'number' ? data.total : rows.length }\n\t\t\t\t} catch (e) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn('[CnSearchPage] could not search', source.schema, e)\n\t\t\t\t\treturn { source, rows: [], total: 0 }\n\t\t\t\t}\n\t\t\t}))\n\t\t\tif (run !== this.sourceRun) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst rows = { ...this.sourceRows }\n\t\t\tconst totals = { ...this.sourceTotals }\n\t\t\tfor (const { source, rows: list, total } of found) {\n\t\t\t\trows[source.key] = list.map((row) => this.resultOf(source, row))\n\t\t\t\ttotals[source.key] = total\n\t\t\t}\n\t\t\tthis.sourceRows = rows\n\t\t\tthis.sourceTotals = totals\n\t\t\tthis.sourceLoading = false\n\t\t},\n\n\t\t/**\n\t\t * One found object as a result row.\n\t\t *\n\t\t * @param {object} source The list it came from.\n\t\t * @param {object} row The object.\n\t\t * @return {object} `{ id, title, snippet, schema, route }`.\n\t\t */\n\t\tresultOf(source, row) {\n\t\t\tconst id = row && (row.id || row['@self']?.id)\n\t\t\tconst titleField = source.titleField || ''\n\t\t\tconst title = (titleField && row[titleField]) || row.title || row.name || id\n\t\t\tconst snippet = source.snippetField ? row[source.snippetField] : undefined\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\ttitle: typeof title === 'string' ? title : String(title ?? ''),\n\t\t\t\t...(typeof snippet === 'string' && snippet !== '' ? { snippet } : {}),\n\t\t\t\tschema: this.shown(source.label) || source.schema,\n\t\t\t\troute: source.route || '',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Forward a result click.\n\t\t *\n\t\t * @param {object} result The clicked result.\n\t\t * @return {void}\n\t\t */\n\t\tonResultClick(result) {\n\t\t\t/**\n\t\t\t * @event result-click Emitted on result-row click.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('result-click', result)\n\t\t\tif (this.searchesSources && result && result.route && result.id && this.$router) {\n\t\t\t\tthis.$router.push({ name: result.route, params: { id: result.id } })\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-search-page {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\tpadding: 16px;\n}\n\n/* Reserve room for NcAppNavigationToggle (matches CnPageRenderer / CnPageHeader). */\n.cn-search-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-search-page__title {\n\tmargin: 0 0 8px;\n\tfont-size: 1.4em;\n}\n\n.cn-search-page__query {\n\tdisplay: flex;\n\tgap: 8px;\n}\n\n.cn-search-page__input {\n\tflex: 1 1 auto;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tfont-size: 1em;\n}\n\n.cn-search-page__submit {\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text, #fff);\n\tborder: none;\n\tpadding: 8px 16px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n}\n\n.cn-search-page__body {\n\tdisplay: grid;\n\tgrid-template-columns: 240px 1fr;\n\tgap: 16px;\n}\n\n.cn-search-page__body:has(:not(.cn-search-page__facets)) {\n\tgrid-template-columns: 1fr;\n}\n\n.cn-search-page__facets {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 12px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-search-page__facets-title {\n\tmargin: 0;\n\tfont-size: 1em;\n}\n\n.cn-search-page__facet-title {\n\tmargin: 0 0 4px;\n\tfont-size: 0.9em;\n}\n\n.cn-search-page__facet-options {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-search-page__facet-option {\n\tdisplay: flex;\n\tgap: 4px;\n}\n\n.cn-search-page__facet-option label {\n\tdisplay: flex;\n\tgap: 6px;\n\talign-items: center;\n\tcursor: pointer;\n\tfont-size: 0.9em;\n}\n\n.cn-search-page__facet-count {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-search-page__clear-facets {\n\tbackground: none;\n\tborder: 1px solid var(--color-border);\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tfont-size: 0.85em;\n\talign-self: flex-start;\n}\n\n.cn-search-page__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-search-page__result {\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tbackground: var(--color-main-background);\n}\n\n.cn-search-page__result:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-search-page__result-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-search-page__result-title {\n\tfont-weight: 600;\n}\n\n.cn-search-page__result-badge {\n\tfont-size: 0.75em;\n\tpadding: 1px 8px;\n\tborder-radius: 9999px;\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\ttext-transform: uppercase;\n\tletter-spacing: 0.05em;\n}\n\n.cn-search-page__result-snippet {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-search-page__result-subtitle {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-search-page__empty,\n.cn-search-page__idle,\n.cn-search-page__loading {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 24px 0;\n\ttext-align: center;\n\tfont-style: italic;\n}\n\n.cn-search-page__more {\n\tmargin-top: 12px;\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createElementVNode","_toDisplayString","_Fragment","_renderList","_renderSlot"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnTabsWidget.vue2.js';
2
2
  import { render } from './CnTabsWidget.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-5704a09b";
5
+ script.__scopeId = "data-v-c0b67e7e";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -13,7 +13,7 @@ import { detailWidgetVisible } from '../../utils/detailWidgetVisibility.js';
13
13
  import { fetchListTotal } from '../../utils/fetchFilterCounts.js';
14
14
  import { isActivityWidget } from '../../utils/headerMeta.js';
15
15
  import { PANEL_ACTION_SINK } from '../../utils/panelActions.js';
16
- import { isLocallyDecidableVisibleWhen, isWorkspaceCondition, evaluateWorkspaceCondition, evaluateVisibleWhenLocal, evaluateVisibleWhen } from '../../utils/visibleWhen.js';
16
+ import { isLocallyDecidableVisibleWhen, isWorkspaceCondition, evaluateWorkspaceCondition, evaluateVisibleWhenLocal, evaluateVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js';
17
17
  import { widgetTitleOf } from '../../utils/widgetDispatch.js';
18
18
  import __sfc_main from '../CnActionsMenu/CnActionsMenu.vue.js';
19
19
 
@@ -188,7 +188,9 @@ var script = {
188
188
  * counts a list on the server: `"widget"` counts the child widget's
189
189
  * own list (`content.register`, `schema`, `filter`, tokens such as
190
190
  * `@objectId` resolved), an object `{ register, schema, filter? }`
191
- * names the list; `count` and `countField` win over it. `overflow: true`
191
+ * names the list, and `{ endpoint, field? }` counts the list (or reads
192
+ * the number) an endpoint answers with, `@objectId` resolved; `count`
193
+ * and `countField` win over it. `overflow: true`
192
194
  * lists the tab under "More".
193
195
  *
194
196
  * A tab with `widgetIds` can lay them out in a grid instead of one under
@@ -205,7 +207,7 @@ var script = {
205
207
  * `moreLabel` names that menu. `variant: "segmented"` draws the strip
206
208
  * as a pill switch (CnTabs' segmented variant); `line` is the default.
207
209
  *
208
- * @type {{ tabs?: Array<{widgetId?: string, widgetIds?: string[], label?: string, icon?: string, count?: number, countField?: string, countFrom?: ('widget'|{register: string, schema: string, filter?: object}), overflow?: boolean, default?: boolean, columns?: number, layout?: Array<{widgetId: string, gridWidth: number}>}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}
210
+ * @type {{ tabs?: Array<{widgetId?: string, widgetIds?: string[], label?: string, icon?: string, count?: number, countField?: string, countFrom?: ('widget'|{register: string, schema: string, filter?: object}|{endpoint: string, field?: string}), overflow?: boolean, default?: boolean, columns?: number, layout?: Array<{widgetId: string, gridWidth: number}>}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}
209
211
  */
210
212
  content: {
211
213
  type: Object,
@@ -962,7 +964,7 @@ var script = {
962
964
  * no list.
963
965
  *
964
966
  * @param {object|string} tab The `content.tabs[]` entry.
965
- * @return {{register: string, schema: string, filter: object}|null} The list.
967
+ * @return {{register: string, schema: string, filter: object}|{endpoint: string, field: string}|null} The list, or the endpoint to count.
966
968
  * @spec openspec/changes/screens-tab-counts-parity/specs/tab-counts/spec.md#requirement-a-tab-can-count-its-list
967
969
  */
968
970
  countSourceOf(tab) {
@@ -978,6 +980,9 @@ var script = {
978
980
  .filter((content) => content && content.register && content.schema);
979
981
  source = lists.length ? lists[0] : null;
980
982
  } else if (from && typeof from === 'object') {
983
+ if (typeof from.endpoint === 'string' && from.endpoint !== '') {
984
+ return { endpoint: from.endpoint, field: typeof from.field === 'string' ? from.field : '' }
985
+ }
981
986
  source = from;
982
987
  }
983
988
  if (!source || typeof source.register !== 'string' || source.register === '' || typeof source.schema !== 'string' || source.schema === '') {
@@ -1004,13 +1009,38 @@ var script = {
1004
1009
  if (!source) {
1005
1010
  return
1006
1011
  }
1007
- const total = await fetchListTotal(source.register, source.schema, source.filter, ctx).catch(() => null);
1012
+ const total = source.endpoint
1013
+ ? await this.countEndpoint(source, ctx)
1014
+ : await fetchListTotal(source.register, source.schema, source.filter, ctx).catch(() => null);
1008
1015
  if (seq === this.countSeq) {
1009
1016
  this.widgetCounts = { ...this.widgetCounts, [index]: total };
1010
1017
  }
1011
1018
  }));
1012
1019
  },
1013
1020
 
1021
+ /**
1022
+ * Count what an endpoint answers: the list at `field` (or the body),
1023
+ * or the number there. Null when the request fails or nothing countable
1024
+ * comes back.
1025
+ *
1026
+ * @param {{endpoint: string, field: string}} source The endpoint and the path to read.
1027
+ * @param {object} ctx The record context its tokens resolve against.
1028
+ * @return {Promise<number|null>} The count.
1029
+ * @spec openspec/changes/screens-tab-counts-parity/specs/tab-counts/spec.md#requirement-a-tab-can-count-its-list
1030
+ */
1031
+ async countEndpoint(source, ctx) {
1032
+ try {
1033
+ const value = await readVisibleWhenValue({ endpoint: source.endpoint, field: source.field }, ctx);
1034
+ if (Array.isArray(value)) {
1035
+ return value.length
1036
+ }
1037
+ const number = Number(value);
1038
+ return value !== null && value !== '' && Number.isFinite(number) ? number : null
1039
+ } catch {
1040
+ return null
1041
+ }
1042
+ },
1043
+
1014
1044
  /** @return {string} The route hash without its `#`, or ''. */
1015
1045
  currentHash() {
1016
1046
  const fromRoute = this.$route && typeof this.$route.hash === 'string' ? this.$route.hash : '';