@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.
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +2 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +6 -6
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +2 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +18 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +14 -8
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +9 -4
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnMailTab/CnMailTab.vue2.js +2 -1
- package/dist/esm/components/CnMailTab/CnMailTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +71 -0
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +25 -9
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +11 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSearchPage/CnSearchPage.vue.js +1 -1
- package/dist/esm/components/CnSearchPage/CnSearchPage.vue2.js +161 -5
- package/dist/esm/components/CnSearchPage/CnSearchPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSearchPage/CnSearchPage.vue3.js +9 -9
- package/dist/esm/components/CnSearchPage/CnSearchPage.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +35 -5
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue.js +9 -0
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue.js.map +1 -0
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js +435 -0
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js.map +1 -0
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js +300 -0
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js.map +1 -0
- package/dist/esm/composables/useEndpointSource.js +1 -36
- package/dist/esm/composables/useEndpointSource.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +57 -336
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +18 -155
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +2 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +2 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +308 -229
- package/dist/esm/utils/actionsDispatcher.js +3 -2
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/interpolateUrlTokens.js +43 -0
- package/dist/esm/utils/interpolateUrlTokens.js.map +1 -0
- package/dist/esm/utils/labelsByField.js +72 -0
- package/dist/esm/utils/labelsByField.js.map +1 -0
- package/dist/esm/utils/listCreateAction.js +91 -0
- package/dist/esm/utils/listCreateAction.js.map +1 -0
- package/dist/esm/utils/transitionInputs.js +493 -0
- package/dist/esm/utils/transitionInputs.js.map +1 -0
- package/dist/esm/utils/visibleWhen.js +9 -2
- package/dist/esm/utils/visibleWhen.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +6690 -5404
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +308 -229
- package/l10n/en.json +2 -1
- package/l10n/nl.json +2 -1
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.vue +6 -6
- package/src/components/CnDashboardPage/CnDashboardPage.vue +27 -5
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +11 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +9 -4
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +86 -0
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +11 -2
- package/src/components/CnSearchPage/CnSearchPage.vue +170 -14
- package/src/components/CnTabsWidget/CnTabsWidget.vue +35 -5
- package/src/components/CnTransitionInputFields/CnTransitionInputFields.vue +750 -0
- package/src/components/CnTransitionInputFields/index.js +11 -0
- package/src/components/index.js +1 -0
- package/src/composables/useEndpointSource.js +3 -36
- package/src/dialogs/CnTransitionInputDialog.vue +73 -465
- package/src/index.js +2 -0
- package/src/utils/interpolateUrlTokens.js +40 -0
- package/src/utils/labelsByField.js +69 -0
- package/src/utils/listCreateAction.js +87 -0
- package/src/utils/transitionInputs.js +489 -0
- 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.
|
|
117
|
-
if (Array.isArray(this.
|
|
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.
|
|
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.
|
|
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.
|
|
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":";;AAkGA;;;AAGC;AACA;;;;;AAKA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;;AAED,aAAe;AACd,CAAA,IAAI,EAAE,cAAc;;AAEpB,CAAA,MAAM,EAAE;AACP;EACA,WAAW,EAAE,EAAE,OAAO,EAAE,IAAG,EAAG;EAC9B;;AAED,CAAA,KAAK,EAAE;AACN;AACA,EAAA,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU;AAC1C;;AAEC;AACA;AACA;;;AAGD,EAAA,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAC,EAAG;;AAEpC,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW;;AAEjD,EAAA,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB;;AAEpD,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU;AAChD;AACC;AACA;AACA;AACA;AACA;;AAED,EAAA,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,IAAI;AAC1C;;;AAGC;AACA;AACA;;AAED,EAAA,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG;AACnD;;;AAGC;AACA;AACA;;AAED,EAAA,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,IAAI;;AAE3C,EAAA,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG;;AAExC,EAAA,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO;;EAE1C,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAkB,EAAG;;EAE1D,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAwB,EAAG;;EAE/D,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAW,EAAG;;AAErD,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW;;EAEjD,gBAAgB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAc,EAAG;AAC5D;;AAEC;AACA;AACA;;AAED,EAAA,eAAe,EAAE;GAChB,IAAI,EAAE,QAAQ;GACd,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAE,IAAG,EAAE,KAAK,CAAA,SAAU,CAAC;GAClE;;AAED;;;;;;;AAOC;AACA;;AAED,EAAA,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,IAAI;;AAE3C,EAAA,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ;EAC5C;;AAED,CAAA,KAAK,EAAE,CAAC,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,QAAQ,EAAE,cAAc,CAAC;AAClF,CAAA,IAAI,GAAG;AACN,EAAA,OAAO;AACN,GAAA,UAAU,EAAE,IAAI,CAAC,KAAK;AACtB,GAAA,WAAW,EAAE,KAAK;;GAElB,UAAU,EAAE,EAAE;AACd,GAAA,YAAY,EAAE,EAAE;AAChB,GAAA,YAAY,EAAE,EAAE;AAChB,GAAA,aAAa,EAAE,KAAK;GACpB,SAAS,EAAE,CAAC;AACb,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;;AAET,EAAA,eAAe,GAAG;GACjB,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAA,IAAK,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,MAAK,IAAK,MAAM,CAAC,QAAS,IAAG,MAAM,CAAC,MAAM,CAAA;GAC9G;;;EAGD,UAAU,GAAG;GACZ,OAAO,CAAC,IAAI,CAAC,OAAQ,IAAG,EAAE;KACxB,MAAM,CAAC,CAAC,MAAM,KAAK,MAAK,IAAK,MAAM,CAAC,QAAS,IAAG,MAAM,CAAC,MAAM,CAAA;KAC7D,GAAG,CAAC,CAAC,MAAM,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,EAAG,IAAG,MAAM,CAAC,MAAO,EAAC,CAAC,CAAA;GAClE;;AAED;AACA,EAAA,eAAe,GAAG;AACjB,GAAA,OAAO,IAAI,CAAC,eAAc,GAAI,IAAI,CAAC,YAAa,IAAG,IAAI,CAAC,YAAW,IAAK,EAAE,CAAA;GAC1E;;AAED;EACA,WAAW,GAAG;GACb,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE;AAC1B,IAAA,OAAO,IAAI,CAAC,MAAK;AAClB,IAAA;AACA,GAAA,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE;AACtB,IAAA,OAAO,EAAC;AACT,IAAA;AACA,GAAA,OAAO,CAAC;IACP,GAAG,EAAE,MAAM;AACX,IAAA,KAAK,EAAE,IAAI,CAAC,SAAS;IACrB,QAAQ,EAAE,IAAI;AACd,IAAA,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,MAAM,MAAM;AACzC,KAAA,KAAK,EAAE,MAAM,CAAC,GAAG;AACjB,KAAA,KAAK,EAAE,MAAM,CAAC,KAAK;AACnB,KAAA,IAAI,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,CAAE,KAAI,QAAS,GAAE,EAAE,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,CAAA,KAAM,EAAE,CAAC;AACvG,KAAC,CAAC,CAAC;IACH,CAAA;GACD;;AAED;AACA,EAAA,YAAY,GAAG;GACd,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE;AAC1B,IAAA,OAAO,IAAI,CAAC,OAAM;AACnB,IAAA;AACA,GAAA,OAAO,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,MAAM,KAAK,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAA,IAAK,EAAE,CAAA;GAC/E;;AAED;EACA,UAAU,GAAG;GACZ,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE;AAC1B,IAAA,OAAO,IAAI,CAAC,UAAS;AACtB,IAAA;AACA,GAAA,OAAO,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,CAAE,IAAG,CAAC,CAAC,EAAE,CAAC,CAAA;GAC/F;;AAED;AACA,EAAA,YAAY,GAAG;GACd,OAAO,IAAI,CAAC,eAAc,GAAI,IAAI,CAAC,aAAc,GAAE,IAAI,CAAC,OAAM;GAC9D;;AAED;AACA,EAAA,aAAa,GAAG;AACf,GAAA,MAAM,MAAK,GAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAA,GAAI,IAAI,CAAC,eAAe,CAAC,IAAG,GAAI,GAAC;GACvF,OAAO,MAAM,CAAC,MAAO,KAAI,CAAE,GAAE,IAAI,CAAC,UAAW,GAAE,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;GAC7G;;AAED;AACC;AACA;AACA;;AAED,EAAA,eAAe,GAAG;GACjB,KAAK,MAAM,CAAE,IAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE;AAClD,IAAA,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAE,IAAG,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,MAAO,GAAE,CAAC,EAAE;AACjF,KAAA,OAAO,IAAG;AACX,KAAA;AACD,IAAA;GACA,OAAO,KAAI;GACX;EACD;;AAED,CAAA,KAAK,EAAE;EACN,KAAK,CAAC,IAAI,EAAE;GACX,IAAI,SAAS,IAAI,CAAC,UAAU,EAAE;AAC7B,IAAA,IAAI,CAAC,aAAa,KAAG;AACtB,IAAA;GACA;EACD;;AAED,CAAA,OAAO,EAAE;AACR;;AAEC;AACA;AACA;;EAED,KAAK,CAAC,IAAI,EAAE;GACX,OAAO,OAAO,IAAI,CAAC,WAAU,KAAM,UAAS,IAAK,OAAO,IAAK,KAAI,QAAO,IAAK,IAAG,KAAM,EAAC,GAAI,IAAI,CAAC,WAAW,CAAC,IAAI,CAAE,GAAE,IAAG;GACvH;;AAED;AACC;AACA;AACA;;;EAGD,MAAM,CAAC,IAAI,EAAE;GACZ,OAAO,SAAS,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAE,IAAG,SAAQ;GACpD;;AAED;AACC;AACA;;AAEA;AACA;;EAED,aAAa,CAAC,GAAG,EAAE,KAAK,EAAE;AACzB,GAAA,MAAM,GAAI,GAAE,IAAI,CAAC,eAAe,CAAC,GAAG,EAAA;AACpC,GAAA,OAAO,KAAK,CAAC,OAAO,CAAC,GAAG,CAAA,IAAK,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAA;GAC/C;;AAED;AACC;AACA;;AAEA;;;;;AAKD,EAAA,WAAW,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE;AAC1C,GAAA,MAAM,IAAK,GAAE,EAAE,GAAG,IAAI,CAAC,eAAgB,GAAA;GACvC,IAAI,CAAC,QAAQ,EAAE;IACd,IAAI,CAAC,GAAG,CAAA,GAAI,OAAQ,GAAE,CAAC,KAAK,CAAE,GAAE,GAAC;IAChC,MAAK;IACN,MAAM,GAAE,GAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAE,GAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAE,GAAE,GAAC;IACzD,MAAM,GAAI,GAAE,GAAG,CAAC,OAAO,CAAC,KAAK,EAAA;AAC7B,IAAA,IAAI,OAAQ,IAAG,GAAI,GAAE,CAAC,EAAE;AACvB,KAAA,GAAG,CAAC,IAAI,CAAC,KAAK,EAAA;KACb,MAAK,IAAI,CAAC,OAAQ,IAAG,GAAI,IAAG,CAAC,EAAE;AAChC,KAAA,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,EAAA;AAClB,KAAA;IACA,IAAI,CAAC,GAAG,CAAA,GAAI,IAAE;AACf,IAAA;GACA,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAA;GAC1B;;AAED;AACC;AACA;;;EAGD,WAAW,GAAG;GACb,IAAI,CAAC,gBAAgB,CAAC,EAAE,EAAA;GACxB;;EAED,gBAAgB,CAAC,IAAI,EAAE;AACtB;;AAEC;;AAED,GAAA,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,IAAI,EAAA;GAChC,IAAI,IAAI,CAAC,eAAe,EAAE;AACzB,IAAA,IAAI,CAAC,eAAe,KAAG;AACxB,IAAA;AACA,GAAA,IAAI,CAAC,UAAU,GAAC;GAChB;;AAED;;AAEC;;;AAGD,EAAA,aAAa,GAAG;AACf,GAAA,IAAI,CAAC,eAAe,GAAC;AACrB,GAAA,IAAI,CAAC,UAAU,GAAC;GAChB;;AAED;;;AAGC;;;AAGD,EAAA,YAAY,GAAG;AACd,GAAA,IAAI,CAAC,eAAe,GAAC;GACrB;;AAED,EAAA,eAAe,GAAG;AACjB;;;;AAIA,GAAA,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,CAAC,UAAU,EAAA;;AAE1C,GAAA,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,CAAC,UAAU,EAAA;GAC1C;;EAED,UAAU,GAAG;AACZ,GAAA,IAAI,CAAC,cAAc,KAAG;AACtB;AACC;;;;AAID,GAAA,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI,CAAC,eAAc,IAAK,EAAA;GACpF,IAAI,IAAI,CAAC,eAAe,EAAE;AACzB,IAAA,IAAI,CAAC,aAAa,GAAC;AACpB,IAAA;GACA;;AAED;;;AAGC;;;AAGD,EAAA,MAAM,aAAa,GAAG;GACrB,MAAM,GAAE,GAAI,IAAI,CAAC,SAAQ,GAAI,EAAA;AAC7B,GAAA,IAAI,CAAC,SAAU,GAAE,IAAE;GACnB,IAAI,CAAC,gBAAgB,KAAG;GACxB,MAAM,CAAC,EAAE,OAAO,EAAE,KAAI,EAAG,EAAE,EAAE,WAAU,EAAG,CAAE,GAAE,MAAM,OAAO,CAAC,GAAG,CAAC;IAC/D,OAAO,kBAAkB,CAAC;IAC1B,OAAO,mBAAmB,CAAC;IAC3B,EAAA;AACD,GAAA,MAAM,KAAM,GAAE,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,MAAM,KAAK;IACxE,MAAM,GAAE,GAAI,WAAW,CAAC,oDAAoD,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,MAAK,EAAG,EAAA;AAClI,IAAA,MAAM,KAAM,GAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAA,GAAI,CAAE,GAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA,GAAI,GAAC;AAC7E,IAAA,IAAI;AACH,KAAA,MAAM,QAAS,GAAE,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,KAAI,IAAK,EAAA;KAC7F,MAAM,OAAO,YAAY,QAAQ,CAAC,IAAG,GAAI,QAAQ,CAAC,IAAK,GAAE,GAAC;KAC1D,MAAM,IAAK,GAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAA,GAAI,IAAI,CAAC,OAAQ,GAAE,GAAC;AAC3D,KAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,IAAI,CAAC,KAAI,KAAM,QAAS,GAAE,IAAI,CAAC,KAAI,GAAI,IAAI,CAAC,MAAO,EAAA;AACzF,KAAA,CAAE,OAAO,CAAC,EAAE;AACX;KACA,OAAO,CAAC,IAAI,CAAC,iCAAiC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,EAAA;KAChE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,CAAE,EAAA;AACrC,KAAA;AACD,IAAC,CAAC,EAAA;GACF,IAAI,QAAQ,IAAI,CAAC,SAAS,EAAE;IAC3B,MAAK;AACN,IAAA;GACA,MAAM,IAAK,GAAE,EAAE,GAAG,IAAI,CAAC,UAAW,GAAA;AAClC,GAAA,MAAM,MAAK,GAAI,EAAE,GAAG,IAAI,CAAC,YAAa,GAAA;AACtC,GAAA,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAM,EAAA,IAAK,KAAK,EAAE;IAClD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAE,GAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,EAAA;IAC/D,MAAM,CAAC,MAAM,CAAC,GAAG,IAAI,MAAI;AAC1B,IAAA;AACA,GAAA,IAAI,CAAC,aAAa,KAAG;GACrB,IAAI,CAAC,YAAW,GAAI,OAAK;GACzB,IAAI,CAAC,gBAAgB,MAAI;GACzB;;AAED;;AAEC;AACA;;;;AAID,EAAA,QAAQ,CAAC,MAAM,EAAE,GAAG,EAAE;AACrB,GAAA,MAAM,KAAK,GAAE,KAAM,GAAG,CAAC,EAAG,IAAG,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,EAAA;AAC7C,GAAA,MAAM,UAAS,GAAI,MAAM,CAAC,UAAW,IAAG,GAAC;AACzC,GAAA,MAAM,KAAM,GAAE,CAAC,UAAW,IAAG,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC,KAAM,IAAG,GAAG,CAAC,IAAK,IAAG,GAAC;AAC3E,GAAA,MAAM,OAAQ,GAAE,MAAM,CAAC,YAAa,GAAE,GAAG,CAAC,MAAM,CAAC,YAAY,CAAE,GAAE,UAAQ;AACzE,GAAA,OAAO;AACN,IAAA,EAAE;IACF,KAAK,EAAE,OAAO,KAAI,KAAM,QAAS,GAAE,KAAM,GAAE,MAAM,CAAC,SAAS,EAAE,CAAC;IAC9D,IAAI,OAAO,OAAQ,KAAI,QAAO,IAAK,OAAQ,KAAI,EAAC,GAAI,EAAE,OAAM,EAAI,GAAE,EAAE,CAAC;IACrE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAE,IAAG,MAAM,CAAC,MAAM;IACjD,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE;AAC1B,IAAA;GACA;;AAED;AACC;AACA;AACA;;;AAGD,EAAA,aAAa,CAAC,MAAM,EAAE;AACrB;AACC;;;AAGD,GAAA,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,MAAM,EAAA;AACjC,GAAA,IAAI,IAAI,CAAC,eAAc,IAAK,MAAO,IAAG,MAAM,CAAC,KAAM,IAAG,MAAM,CAAC,EAAG,IAAG,IAAI,CAAC,OAAO,EAAE;IAChF,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAG,EAAA,EAAG,EAAA;AACpE,IAAA;GACA;EACD;AACF;;;;"}
|
|
@@ -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
|
-
($
|
|
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($
|
|
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
|
-
($
|
|
140
|
+
($options.shownLoading)
|
|
141
141
|
? (openBlock(), createElementBlock("div", _hoisted_14, toDisplayString($props.loadingLabel), 1 /* TEXT */))
|
|
142
|
-
: ($
|
|
142
|
+
: ($options.shownResults.length > 0)
|
|
143
143
|
? (openBlock(), createElementBlock("ol", _hoisted_15, [
|
|
144
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList($
|
|
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
|
-
($
|
|
174
|
-
? (openBlock(), createElementBlock("div", _hoisted_24, toDisplayString($props.totalCountLabel($
|
|
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-
|
|
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
|
|
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 =
|
|
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 : '';
|