@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
package/l10n/en.json
CHANGED
|
@@ -1974,7 +1974,8 @@
|
|
|
1974
1974
|
"Other open tasks": "Other open tasks",
|
|
1975
1975
|
"Task {title} finished": "Task {title} finished",
|
|
1976
1976
|
"Task {title} is yours": "Task {title} is yours",
|
|
1977
|
-
"Fill in {field} before completing this task": "Fill in {field} before completing this task"
|
|
1977
|
+
"Fill in {field} before completing this task": "Fill in {field} before completing this task",
|
|
1978
|
+
"Search in this list": "Search in this list"
|
|
1978
1979
|
},
|
|
1979
1980
|
"plurals": {
|
|
1980
1981
|
"{count} member": [
|
package/l10n/nl.json
CHANGED
|
@@ -2224,7 +2224,8 @@
|
|
|
2224
2224
|
"{field}, up to": "{field}, tot en met",
|
|
2225
2225
|
"Withheld": "Afgeschermd",
|
|
2226
2226
|
"This value is not shown.": "Deze waarde wordt niet getoond.",
|
|
2227
|
-
"You may not open the linked object, so its value is not shown.": "U mag het gekoppelde object niet openen, daarom wordt de waarde niet getoond."
|
|
2227
|
+
"You may not open the linked object, so its value is not shown.": "U mag het gekoppelde object niet openen, daarom wordt de waarde niet getoond.",
|
|
2228
|
+
"Search in this list": "Zoeken in deze lijst"
|
|
2228
2229
|
},
|
|
2229
2230
|
"plurals": {
|
|
2230
2231
|
"{count} member": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.118.0",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
<input
|
|
28
28
|
type="search"
|
|
29
29
|
class="cn-actions-bar__search-input"
|
|
30
|
-
:placeholder="searchPlaceholder || t('nextcloud-vue', 'Search
|
|
30
|
+
:placeholder="searchPlaceholder || t('nextcloud-vue', 'Search in this list')"
|
|
31
31
|
:value="searchValue"
|
|
32
|
-
:aria-label="searchPlaceholder || t('nextcloud-vue', 'Search')"
|
|
32
|
+
:aria-label="searchPlaceholder || t('nextcloud-vue', 'Search in this list')"
|
|
33
33
|
@input="onSearchInput">
|
|
34
34
|
</div>
|
|
35
35
|
<!--
|
|
@@ -117,9 +117,9 @@
|
|
|
117
117
|
<input
|
|
118
118
|
type="search"
|
|
119
119
|
class="cn-actions-bar__search-input"
|
|
120
|
-
:placeholder="searchPlaceholder || t('nextcloud-vue', 'Search
|
|
120
|
+
:placeholder="searchPlaceholder || t('nextcloud-vue', 'Search in this list')"
|
|
121
121
|
:value="searchValue"
|
|
122
|
-
:aria-label="searchPlaceholder || t('nextcloud-vue', 'Search')"
|
|
122
|
+
:aria-label="searchPlaceholder || t('nextcloud-vue', 'Search in this list')"
|
|
123
123
|
@input="onSearchInput">
|
|
124
124
|
</div>
|
|
125
125
|
<span v-else-if="showCount && hasTotal" class="cn-actions-bar__count">
|
|
@@ -392,9 +392,9 @@
|
|
|
392
392
|
<input
|
|
393
393
|
type="search"
|
|
394
394
|
class="cn-actions-bar__search-input"
|
|
395
|
-
:placeholder="searchPlaceholder || t('nextcloud-vue', 'Search
|
|
395
|
+
:placeholder="searchPlaceholder || t('nextcloud-vue', 'Search in this list')"
|
|
396
396
|
:value="searchValue"
|
|
397
|
-
:aria-label="searchPlaceholder || t('nextcloud-vue', 'Search')"
|
|
397
|
+
:aria-label="searchPlaceholder || t('nextcloud-vue', 'Search in this list')"
|
|
398
398
|
@input="onSearchInput">
|
|
399
399
|
</div>
|
|
400
400
|
<span v-else-if="showCount && hasTotal && !searchInToolbar" class="cn-actions-bar__count">
|
|
@@ -120,6 +120,10 @@
|
|
|
120
120
|
<!-- @slot actions Back-compat alias for `#header-actions`. Prefer `#header-actions` in new code. -->
|
|
121
121
|
<slot name="actions" />
|
|
122
122
|
</template>
|
|
123
|
+
<CnActionButtons
|
|
124
|
+
v-else-if="boardSecondaryHeaderActions.length"
|
|
125
|
+
:actions="boardSecondaryHeaderActions"
|
|
126
|
+
data-testid="cn-dashboard-page-header-actions-secondary" />
|
|
123
127
|
<NcButton
|
|
124
128
|
v-if="allowEdit"
|
|
125
129
|
:variant="isEditing ? 'primary' : 'secondary'"
|
|
@@ -179,14 +183,14 @@
|
|
|
179
183
|
<slot name="action-items" />
|
|
180
184
|
</template>
|
|
181
185
|
</CnActionsMenu>
|
|
182
|
-
<!-- Board look: the row reads
|
|
183
|
-
|
|
184
|
-
rightmost (screens-dashboard-parity). -->
|
|
186
|
+
<!-- Board look: the row reads the secondary header actions, Edit
|
|
187
|
+
layout, Actions, the buildiq square, then the primary header
|
|
188
|
+
actions rightmost (screens-dashboard-parity). -->
|
|
185
189
|
<template v-if="isBoardLook">
|
|
186
190
|
<CnBuildiqEditButton />
|
|
187
191
|
<CnActionButtons
|
|
188
|
-
v-if="
|
|
189
|
-
:actions="
|
|
192
|
+
v-if="boardPrimaryHeaderActions.length"
|
|
193
|
+
:actions="boardPrimaryHeaderActions"
|
|
190
194
|
data-testid="cn-dashboard-page-header-actions" />
|
|
191
195
|
<slot name="header-actions" />
|
|
192
196
|
<slot name="actions" />
|
|
@@ -2135,6 +2139,24 @@ export default {
|
|
|
2135
2139
|
return (this.headerActions || []).map(headerActionAsLink)
|
|
2136
2140
|
},
|
|
2137
2141
|
|
|
2142
|
+
/**
|
|
2143
|
+
* Under the board look the header reads the secondary actions, the
|
|
2144
|
+
* edit toggle and Actions menu, the buildiq square, then the primary
|
|
2145
|
+
* actions, as on an index page (PtDashboard, DqDashboard).
|
|
2146
|
+
*
|
|
2147
|
+
* @return {Array<object>} The header actions that are not `variant: "primary"`.
|
|
2148
|
+
*/
|
|
2149
|
+
boardSecondaryHeaderActions() {
|
|
2150
|
+
return this.linkedHeaderActions.filter((entry) => entry?.variant !== 'primary')
|
|
2151
|
+
},
|
|
2152
|
+
|
|
2153
|
+
/**
|
|
2154
|
+
* @return {Array<object>} The `variant: "primary"` header actions, drawn after the buildiq square.
|
|
2155
|
+
*/
|
|
2156
|
+
boardPrimaryHeaderActions() {
|
|
2157
|
+
return this.linkedHeaderActions.filter((entry) => entry?.variant === 'primary')
|
|
2158
|
+
},
|
|
2159
|
+
|
|
2138
2160
|
/**
|
|
2139
2161
|
* The greeting for the time of day, with the reader's first (or
|
|
2140
2162
|
* full) name when known, or '' when `greeting` is off.
|
|
@@ -348,6 +348,7 @@ import { resolveHeaderLink, widgetHeaderLink } from '../../utils/contentHeaderLi
|
|
|
348
348
|
import { pickDeclaredProps } from '../../utils/declaredProps.js'
|
|
349
349
|
import { unwrapAppConfig } from '../../utils/formatMetric.js'
|
|
350
350
|
import { isActivityWidget } from '../../utils/headerMeta.js'
|
|
351
|
+
import { createActionState, listCreateAction } from '../../utils/listCreateAction.js'
|
|
351
352
|
import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
|
|
352
353
|
import { resolveDeepTokens } from '../../utils/resolveFilterTokens.js'
|
|
353
354
|
import {
|
|
@@ -1254,7 +1255,12 @@ export default {
|
|
|
1254
1255
|
if (!['object-list', 'table'].includes(this.widget?.type)) {
|
|
1255
1256
|
return false
|
|
1256
1257
|
}
|
|
1257
|
-
|
|
1258
|
+
if (this.content.allowCreate === false) {
|
|
1259
|
+
return false
|
|
1260
|
+
}
|
|
1261
|
+
// A declared `createAction` shows only where its visibleWhen holds,
|
|
1262
|
+
// the same answer the list gives for its own Add buttons.
|
|
1263
|
+
return createActionState(this.content, this.object).visible
|
|
1258
1264
|
},
|
|
1259
1265
|
|
|
1260
1266
|
/**
|
|
@@ -1285,6 +1291,10 @@ export default {
|
|
|
1285
1291
|
|
|
1286
1292
|
/** @return {string} Label for the catalog Add action. */
|
|
1287
1293
|
addLabel() {
|
|
1294
|
+
const action = listCreateAction(this.content)
|
|
1295
|
+
if (action && typeof action.label === 'string' && action.label !== '') {
|
|
1296
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(action.label) : action.label
|
|
1297
|
+
}
|
|
1288
1298
|
return t('nextcloud-vue', 'Add')
|
|
1289
1299
|
},
|
|
1290
1300
|
|
|
@@ -1375,6 +1375,7 @@ import { fetchGroupedCounts } from '../../utils/fetchAggregate.js'
|
|
|
1375
1375
|
import { fetchFilterCounts, fetchListTotal } from '../../utils/fetchFilterCounts.js'
|
|
1376
1376
|
import { unwrapAppConfig } from '../../utils/formatMetric.js'
|
|
1377
1377
|
import { buildExportUrl } from '../../utils/indexExportHelpers.js'
|
|
1378
|
+
import { labelsByField } from '../../utils/labelsByField.js'
|
|
1378
1379
|
import { lensUnavailableText } from '../../utils/lensAvailability.js'
|
|
1379
1380
|
import { isModifiedClick, openRowTarget } from '../../utils/linkNavigation.js'
|
|
1380
1381
|
import { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab, viewIdOf } from '../../utils/listLenses.js'
|
|
@@ -2208,14 +2209,16 @@ export default {
|
|
|
2208
2209
|
/**
|
|
2209
2210
|
* Quick filters made from the values a field holds in this list
|
|
2210
2211
|
* (manifest `config.quickFiltersFrom`): `{ field, register?, schema?,
|
|
2211
|
-
* labelField?, allLabel? }`. The page asks OpenRegister for the
|
|
2212
|
+
* valueField?, labelField?, allLabel? }`. The page asks OpenRegister for the
|
|
2212
2213
|
* distinct values of `field` with their counts (within `filter`) and
|
|
2213
2214
|
* draws "All" plus one tab per value, the most frequent first, each
|
|
2214
2215
|
* with its count. With `register` and `schema` the values are ids of
|
|
2215
2216
|
* records of that schema and each tab shows the record's `labelField`
|
|
2216
|
-
* (else its title or name).
|
|
2217
|
+
* (else its title or name). With `valueField` as well, the values are
|
|
2218
|
+
* matched on that field of those records instead of their id (a slug).
|
|
2219
|
+
* Ignored when `quickFilters` is set.
|
|
2217
2220
|
*
|
|
2218
|
-
* @type {{field: string, register?: string, schema?: string, labelField?: string, allLabel?: string}|null}
|
|
2221
|
+
* @type {{field: string, register?: string, schema?: string, valueField?: string, labelField?: string, allLabel?: string}|null}
|
|
2219
2222
|
* @spec openspec/changes/index-quick-filters-from/specs/index-page/spec.md#requirement-quick-filters-can-come-from-a-field-s-values
|
|
2220
2223
|
*/
|
|
2221
2224
|
quickFiltersFrom: {
|
|
@@ -8727,7 +8730,9 @@ export default {
|
|
|
8727
8730
|
groups = []
|
|
8728
8731
|
}
|
|
8729
8732
|
let labels = {}
|
|
8730
|
-
if (from.register && from.schema && groups.length > 0) {
|
|
8733
|
+
if (from.register && from.schema && from.valueField && groups.length > 0) {
|
|
8734
|
+
labels = await labelsByField({ register: from.register, schema: from.schema, field: from.valueField, values: groups.map((g) => g.key), labelField: from.labelField || '' }).catch(() => ({}))
|
|
8735
|
+
} else if (from.register && from.schema && groups.length > 0) {
|
|
8731
8736
|
if (!this._quickFilterLabelResolver) {
|
|
8732
8737
|
this._quickFilterLabelResolver = createRefLabelResolver(() => {
|
|
8733
8738
|
try {
|
|
@@ -243,12 +243,27 @@
|
|
|
243
243
|
<!-- `content.footnote`: a line under the table ("Awb 4:17 en 4:18"),
|
|
244
244
|
through the host translate. Not while the list waits for its
|
|
245
245
|
context or failed to load. -->
|
|
246
|
+
<!-- `content.summary`: counts over the loaded rows ("(1/2 completed)"). -->
|
|
247
|
+
<p
|
|
248
|
+
v-if="resolvedSummary && !waitingForContext && !error && rows.length > 0"
|
|
249
|
+
class="cn-object-list-widget__summary"
|
|
250
|
+
data-testid="cn-object-list-widget-summary">
|
|
251
|
+
{{ resolvedSummary }}
|
|
252
|
+
</p>
|
|
246
253
|
<p
|
|
247
254
|
v-if="resolvedFootnote && !waitingForContext && !error"
|
|
248
255
|
class="cn-object-list-widget__footnote"
|
|
249
256
|
data-testid="cn-object-list-widget-footnote">
|
|
250
257
|
{{ resolvedFootnote }}
|
|
251
258
|
</p>
|
|
259
|
+
<!-- Why the declared create action is withheld on this record
|
|
260
|
+
(`content.createAction.hiddenNote`). -->
|
|
261
|
+
<p
|
|
262
|
+
v-if="createHiddenNote && !waitingForContext && !error"
|
|
263
|
+
class="cn-object-list-widget__hidden-note"
|
|
264
|
+
data-testid="cn-object-list-widget-hidden-note">
|
|
265
|
+
{{ createHiddenNote }}
|
|
266
|
+
</p>
|
|
252
267
|
<!-- Create affordance (ADR-062): every collection carries its Add at
|
|
253
268
|
the bottom of the widget; the host card's Actions menu calls the
|
|
254
269
|
same openCreate() through the public method. Suppressed while the
|
|
@@ -317,11 +332,13 @@ import { useWriteFeedback } from '../../composables/useWriteFeedback.js'
|
|
|
317
332
|
import { actionLink, dispatchAction, hasActionTargetTokens } from '../../utils/actionsDispatcher.js'
|
|
318
333
|
import { buildExportUrl } from '../../utils/indexExportHelpers.js'
|
|
319
334
|
import { followLinkClick, openRowTarget, resolveHref } from '../../utils/linkNavigation.js'
|
|
335
|
+
import { createActionState } from '../../utils/listCreateAction.js'
|
|
320
336
|
import { objectDisplayName, objectFieldValue } from '../../utils/objectName.js'
|
|
321
337
|
import { moveIndex, renumberRows, rowIdOf } from '../../utils/reorderRows.js'
|
|
322
338
|
import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
323
339
|
import { markNewTabHandled } from '../../utils/rowAuxClick.js'
|
|
324
340
|
import { changedKeys } from '../../utils/saveEdited.js'
|
|
341
|
+
import { evaluateVisibleWhenLocal } from '../../utils/visibleWhen.js'
|
|
325
342
|
import { dispatchObjectCreated } from '../../utils/walkthroughSignals.js'
|
|
326
343
|
import { getCurrentUserGroups } from '../../utils/widgetVisibility.js'
|
|
327
344
|
import { CnRowActions } from '../CnRowActions/index.js'
|
|
@@ -447,6 +464,17 @@ export default {
|
|
|
447
464
|
* those carrying a selected value. Client-side over the fetched page,
|
|
448
465
|
* like the rest of this widget's row set.
|
|
449
466
|
*
|
|
467
|
+
* `createAction` (one action of the `rowActions` shape) replaces the
|
|
468
|
+
* list's own create form: every Add, here and in the host card's menu,
|
|
469
|
+
* runs it instead, labelled with its `label`. Its `visibleWhen` is
|
|
470
|
+
* evaluated against the page record, and while it fails the Add is
|
|
471
|
+
* hidden and `hiddenNote` (a string, or `[{ when, text }]` where the
|
|
472
|
+
* first holding entry wins) is shown in its place.
|
|
473
|
+
*
|
|
474
|
+
* `summary` (`{ text, counts }`) is a line under the rows: `{total}` is
|
|
475
|
+
* the number of loaded rows and each `counts` key the number of loaded
|
|
476
|
+
* rows its `visibleWhen`-shaped condition holds for.
|
|
477
|
+
*
|
|
450
478
|
* `hideHeader: true` drops the table's column header row (CnDataTable's
|
|
451
479
|
* `hideHeader`), for a short list that reads as rows, not as a table.
|
|
452
480
|
*
|
|
@@ -1091,9 +1119,55 @@ export default {
|
|
|
1091
1119
|
/** Whether the create affordance renders (on by default; `content.allowCreate: false` opts out). */
|
|
1092
1120
|
allowCreate() {
|
|
1093
1121
|
const c = this.content || {}
|
|
1122
|
+
if (this.createState.action) {
|
|
1123
|
+
return c.allowCreate !== false && this.createState.visible
|
|
1124
|
+
}
|
|
1094
1125
|
return c.allowCreate !== false && Boolean(c.register) && Boolean(c.schema)
|
|
1095
1126
|
},
|
|
1096
1127
|
|
|
1128
|
+
/**
|
|
1129
|
+
* The declared `content.createAction` against the page record: the
|
|
1130
|
+
* action, whether it shows, and the untranslated note when it does not.
|
|
1131
|
+
*
|
|
1132
|
+
* @return {{action: (object|null), visible: boolean, note: string}}
|
|
1133
|
+
*/
|
|
1134
|
+
createState() {
|
|
1135
|
+
return createActionState(this.content, this.objectCtx ? this.objectCtx.object : null)
|
|
1136
|
+
},
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Why the create action is withheld, through the host translate, or ''.
|
|
1140
|
+
*
|
|
1141
|
+
* @return {string}
|
|
1142
|
+
*/
|
|
1143
|
+
createHiddenNote() {
|
|
1144
|
+
const note = this.createState.note
|
|
1145
|
+
const fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k
|
|
1146
|
+
return note ? fn(note) : ''
|
|
1147
|
+
},
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* `content.summary` filled in: `{total}` is the number of loaded rows
|
|
1151
|
+
* and every `counts` key the number of loaded rows its condition holds
|
|
1152
|
+
* for. The text goes through the host translate first, so a catalogue
|
|
1153
|
+
* entry keeps its placeholders.
|
|
1154
|
+
*
|
|
1155
|
+
* @return {string}
|
|
1156
|
+
*/
|
|
1157
|
+
resolvedSummary() {
|
|
1158
|
+
const summary = this.content.summary
|
|
1159
|
+
if (!summary || typeof summary.text !== 'string' || summary.text === '') {
|
|
1160
|
+
return ''
|
|
1161
|
+
}
|
|
1162
|
+
const fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k
|
|
1163
|
+
const values = { total: this.rows.length }
|
|
1164
|
+
const counts = summary.counts && typeof summary.counts === 'object' ? summary.counts : {}
|
|
1165
|
+
for (const [key, cond] of Object.entries(counts)) {
|
|
1166
|
+
values[key] = this.rows.filter((row) => evaluateVisibleWhenLocal(cond, row)).length
|
|
1167
|
+
}
|
|
1168
|
+
return fn(summary.text).replace(/\{(\w+)\}/g, (match, key) => (key in values ? String(values[key]) : match))
|
|
1169
|
+
},
|
|
1170
|
+
|
|
1097
1171
|
/**
|
|
1098
1172
|
* NcDialog size for the create dialog (`content.formSize`).
|
|
1099
1173
|
*
|
|
@@ -1214,6 +1288,11 @@ export default {
|
|
|
1214
1288
|
// `prompt`. It used to print raw, so a Dutch user read the English
|
|
1215
1289
|
// manifest string. A label an app already translated has no key
|
|
1216
1290
|
// of its own in the catalogue and comes back unchanged.
|
|
1291
|
+
const action = this.createState.action
|
|
1292
|
+
if (action && typeof action.label === 'string' && action.label !== '') {
|
|
1293
|
+
const fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k
|
|
1294
|
+
return fn(action.label)
|
|
1295
|
+
}
|
|
1217
1296
|
if (this.content.addLabel) {
|
|
1218
1297
|
const fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k
|
|
1219
1298
|
return fn(this.content.addLabel)
|
|
@@ -1712,6 +1791,13 @@ export default {
|
|
|
1712
1791
|
|
|
1713
1792
|
async openCreate() {
|
|
1714
1793
|
const c = this.content || {}
|
|
1794
|
+
const { action, visible } = this.createState
|
|
1795
|
+
if (action) {
|
|
1796
|
+
if (visible) {
|
|
1797
|
+
this.dispatch(action)
|
|
1798
|
+
}
|
|
1799
|
+
return
|
|
1800
|
+
}
|
|
1715
1801
|
if (!c.schema) {
|
|
1716
1802
|
return
|
|
1717
1803
|
}
|
|
@@ -374,7 +374,7 @@ export default {
|
|
|
374
374
|
|
|
375
375
|
props: {
|
|
376
376
|
/**
|
|
377
|
-
* The list config: `{ register, schema, filter?, sort?, limit?, extend?,
|
|
377
|
+
* The list config: `{ register, schema, filter?, sort?, limit?, offset?, extend?,
|
|
378
378
|
* presentation?, variant?, lead?, title?, meta?, text?, label?, pill?, trailing?, icon?,
|
|
379
379
|
* rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText?,
|
|
380
380
|
* valueLabels?, endpointSource?, bar? }`. `bar` (`{ valueField, maxField?,
|
|
@@ -603,10 +603,16 @@ export default {
|
|
|
603
603
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 5
|
|
604
604
|
},
|
|
605
605
|
|
|
606
|
+
/** @return {number} How many rows of the list to skip first (`content.offset`), 0 by default. */
|
|
607
|
+
offset() {
|
|
608
|
+
const value = Number(this.content?.offset)
|
|
609
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0
|
|
610
|
+
},
|
|
611
|
+
|
|
606
612
|
/** @return {string} What a fetch depends on; a change fetches again. */
|
|
607
613
|
requestKey() {
|
|
608
614
|
const c = this.content || {}
|
|
609
|
-
return JSON.stringify([c.register, c.schema, this.resolvedFilter, this.effectiveSort, this.limit, c.extend, this.waiting])
|
|
615
|
+
return JSON.stringify([c.register, c.schema, this.resolvedFilter, this.effectiveSort, this.limit, this.offset, c.extend, this.waiting])
|
|
610
616
|
},
|
|
611
617
|
|
|
612
618
|
/** @return {Array<object>} The rows ready to draw. */
|
|
@@ -1485,6 +1491,9 @@ export default {
|
|
|
1485
1491
|
const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: c.register, schema: c.schema })
|
|
1486
1492
|
const sort = this.effectiveSort
|
|
1487
1493
|
const params = { _limit: this.limit }
|
|
1494
|
+
if (this.offset > 0) {
|
|
1495
|
+
params._offset = this.offset
|
|
1496
|
+
}
|
|
1488
1497
|
// A rank sort (`sort.rank`) cannot be one `_order`: the list endpoint
|
|
1489
1498
|
// orders a text field alphabetically. It is read group by group below.
|
|
1490
1499
|
const rank = this.rankOf(sort)
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
|
|
22
22
|
<div class="cn-search-page__body">
|
|
23
23
|
<!-- Facet sidebar. -->
|
|
24
|
-
<aside v-if="
|
|
24
|
+
<aside v-if="shownFacets.length > 0" class="cn-search-page__facets" data-testid="cn-search-page-facets">
|
|
25
25
|
<h3 class="cn-search-page__facets-title">
|
|
26
26
|
{{ facetsTitle }}
|
|
27
27
|
</h3>
|
|
28
|
-
<div v-for="facet in
|
|
28
|
+
<div v-for="facet in shownFacets" :key="facet.key" class="cn-search-page__facet">
|
|
29
29
|
<h4 class="cn-search-page__facet-title">
|
|
30
|
-
{{ facet.label || facet.key }}
|
|
30
|
+
{{ shown(facet.label) || facet.key }}
|
|
31
31
|
</h4>
|
|
32
32
|
<ul class="cn-search-page__facet-options">
|
|
33
33
|
<li v-for="opt in facet.options"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
:type="facet.multiple ? 'checkbox' : 'radio'"
|
|
40
40
|
:name="'facet-' + facet.key"
|
|
41
41
|
@change="toggleFacet(facet.key, opt.value, $event.target.checked, facet.multiple)">
|
|
42
|
-
<span>{{ opt.label || opt.value }}</span>
|
|
42
|
+
<span>{{ shown(opt.label) || opt.value }}</span>
|
|
43
43
|
<small v-if="typeof opt.count === 'number'" class="cn-search-page__facet-count">{{ opt.count }}</small>
|
|
44
44
|
</label>
|
|
45
45
|
</li>
|
|
@@ -55,11 +55,11 @@
|
|
|
55
55
|
|
|
56
56
|
<!-- Results pane. -->
|
|
57
57
|
<section class="cn-search-page__results" data-testid="cn-search-page-results">
|
|
58
|
-
<div v-if="
|
|
58
|
+
<div v-if="shownLoading" class="cn-search-page__loading">
|
|
59
59
|
{{ loadingLabel }}
|
|
60
60
|
</div>
|
|
61
|
-
<ol v-else-if="
|
|
62
|
-
<li v-for="result in
|
|
61
|
+
<ol v-else-if="shownResults.length > 0" class="cn-search-page__list">
|
|
62
|
+
<li v-for="result in shownResults"
|
|
63
63
|
:key="result.id || (result.schema + ':' + (result.title || ''))"
|
|
64
64
|
class="cn-search-page__result"
|
|
65
65
|
:data-result-id="result.id"
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
{{ idleLabel }}
|
|
87
87
|
</p>
|
|
88
88
|
|
|
89
|
-
<div v-if="
|
|
90
|
-
{{ totalCountLabel(
|
|
89
|
+
<div v-if="shownTotal > 0 && shownTotal > shownResults.length" class="cn-search-page__more">
|
|
90
|
+
{{ totalCountLabel(shownTotal, shownResults.length) }}
|
|
91
91
|
</div>
|
|
92
92
|
</section>
|
|
93
93
|
</div>
|
|
@@ -192,6 +192,20 @@ export default {
|
|
|
192
192
|
type: Function,
|
|
193
193
|
default: (total, shown) => `Showing ${shown} of ${total} results.`,
|
|
194
194
|
},
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The lists the page searches itself (manifest `config.sources`). Each
|
|
198
|
+
* `{ id?, label, register, schema, route?, titleField?, snippetField?,
|
|
199
|
+
* limit? }`: one request per list with `_search`, results in the order
|
|
200
|
+
* of the lists, and the lists offered as a Type filter with their
|
|
201
|
+
* totals. `route` names the page a result opens, with its id. Empty
|
|
202
|
+
* keeps the page controlled: `results` and `facets` come from props.
|
|
203
|
+
*
|
|
204
|
+
* @type {Array<object>}
|
|
205
|
+
*/
|
|
206
|
+
sources: { type: Array, default: () => [] },
|
|
207
|
+
/** The Type filter's heading when the page searches `sources`. */
|
|
208
|
+
typeLabel: { type: String, default: 'Type' },
|
|
195
209
|
},
|
|
196
210
|
|
|
197
211
|
emits: ['facets-change', 'query-change', 'result-click', 'search', 'update:query'],
|
|
@@ -199,18 +213,88 @@ export default {
|
|
|
199
213
|
return {
|
|
200
214
|
localQuery: this.query,
|
|
201
215
|
hasSearched: false,
|
|
216
|
+
// What a `sources` search found, per list, and the filter it keeps.
|
|
217
|
+
sourceRows: {},
|
|
218
|
+
sourceTotals: {},
|
|
219
|
+
sourceFacets: {},
|
|
220
|
+
sourceLoading: false,
|
|
221
|
+
sourceRun: 0,
|
|
202
222
|
}
|
|
203
223
|
},
|
|
204
224
|
|
|
205
225
|
computed: {
|
|
226
|
+
/** @return {boolean} Whether the page searches its own `sources`. */
|
|
227
|
+
searchesSources() {
|
|
228
|
+
return Array.isArray(this.sources) && this.sources.some((source) => source && source.register && source.schema)
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/** @return {Array<object>} The lists searched, each with its key. */
|
|
232
|
+
sourceList() {
|
|
233
|
+
return (this.sources || [])
|
|
234
|
+
.filter((source) => source && source.register && source.schema)
|
|
235
|
+
.map((source) => ({ ...source, key: source.id || source.schema }))
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/** @return {Record<string, Array<string>>} The active filter, the page's own when it searches `sources`. */
|
|
239
|
+
effectiveFacets() {
|
|
240
|
+
return this.searchesSources ? this.sourceFacets : (this.activeFacets || {})
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
/** @return {Array<object>} The facets shown: the Type filter of the lists, else the `facets` prop. */
|
|
244
|
+
shownFacets() {
|
|
245
|
+
if (!this.searchesSources) {
|
|
246
|
+
return this.facets
|
|
247
|
+
}
|
|
248
|
+
if (!this.hasSearched) {
|
|
249
|
+
return []
|
|
250
|
+
}
|
|
251
|
+
return [{
|
|
252
|
+
key: 'type',
|
|
253
|
+
label: this.typeLabel,
|
|
254
|
+
multiple: true,
|
|
255
|
+
options: this.sourceList.map((source) => ({
|
|
256
|
+
value: source.key,
|
|
257
|
+
label: source.label,
|
|
258
|
+
...(typeof this.sourceTotals[source.key] === 'number' ? { count: this.sourceTotals[source.key] } : {}),
|
|
259
|
+
})),
|
|
260
|
+
}]
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
/** @return {Array<object>} The results shown. */
|
|
264
|
+
shownResults() {
|
|
265
|
+
if (!this.searchesSources) {
|
|
266
|
+
return this.results
|
|
267
|
+
}
|
|
268
|
+
return this.activeSources.flatMap((source) => this.sourceRows[source.key] || [])
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
/** @return {number} The total of the results shown. */
|
|
272
|
+
shownTotal() {
|
|
273
|
+
if (!this.searchesSources) {
|
|
274
|
+
return this.totalCount
|
|
275
|
+
}
|
|
276
|
+
return this.activeSources.reduce((sum, source) => sum + (this.sourceTotals[source.key] || 0), 0)
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
/** @return {boolean} Whether a search is running. */
|
|
280
|
+
shownLoading() {
|
|
281
|
+
return this.searchesSources ? this.sourceLoading : this.loading
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
/** @return {Array<object>} The lists the Type filter leaves in, all when none is picked. */
|
|
285
|
+
activeSources() {
|
|
286
|
+
const picked = Array.isArray(this.effectiveFacets.type) ? this.effectiveFacets.type : []
|
|
287
|
+
return picked.length === 0 ? this.sourceList : this.sourceList.filter((source) => picked.includes(source.key))
|
|
288
|
+
},
|
|
289
|
+
|
|
206
290
|
/**
|
|
207
291
|
* Whether any facet has at least one active value.
|
|
208
292
|
*
|
|
209
293
|
* @return {boolean}
|
|
210
294
|
*/
|
|
211
295
|
hasActiveFacets() {
|
|
212
|
-
for (const k of Object.keys(this.
|
|
213
|
-
if (Array.isArray(this.
|
|
296
|
+
for (const k of Object.keys(this.effectiveFacets)) {
|
|
297
|
+
if (Array.isArray(this.effectiveFacets[k]) && this.effectiveFacets[k].length > 0) {
|
|
214
298
|
return true
|
|
215
299
|
}
|
|
216
300
|
}
|
|
@@ -255,7 +339,7 @@ export default {
|
|
|
255
339
|
* @return {boolean}
|
|
256
340
|
*/
|
|
257
341
|
isFacetActive(key, value) {
|
|
258
|
-
const arr = this.
|
|
342
|
+
const arr = this.effectiveFacets[key]
|
|
259
343
|
return Array.isArray(arr) && arr.includes(value)
|
|
260
344
|
},
|
|
261
345
|
|
|
@@ -269,7 +353,7 @@ export default {
|
|
|
269
353
|
* @return {void}
|
|
270
354
|
*/
|
|
271
355
|
toggleFacet(key, value, checked, multiple) {
|
|
272
|
-
const next = { ...this.
|
|
356
|
+
const next = { ...this.effectiveFacets }
|
|
273
357
|
if (!multiple) {
|
|
274
358
|
next[key] = checked ? [value] : []
|
|
275
359
|
} else {
|
|
@@ -300,6 +384,9 @@ export default {
|
|
|
300
384
|
* @type {Record<string,Array<string>>}
|
|
301
385
|
*/
|
|
302
386
|
this.$emit('facets-change', next)
|
|
387
|
+
if (this.searchesSources) {
|
|
388
|
+
this.sourceFacets = next
|
|
389
|
+
}
|
|
303
390
|
this.emitSearch()
|
|
304
391
|
},
|
|
305
392
|
|
|
@@ -340,7 +427,73 @@ export default {
|
|
|
340
427
|
* Combines query + active facets.
|
|
341
428
|
* @type {{ query: string, facets: Record<string,Array<string>> }}
|
|
342
429
|
*/
|
|
343
|
-
this.$emit('search', { query: this.localQuery, facets: { ...this.
|
|
430
|
+
this.$emit('search', { query: this.localQuery, facets: { ...this.effectiveFacets } })
|
|
431
|
+
if (this.searchesSources) {
|
|
432
|
+
this.searchSources()
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Search the lists the Type filter leaves in, one request each, and
|
|
438
|
+
* keep what each found. A search started later wins over this one.
|
|
439
|
+
*
|
|
440
|
+
* @return {Promise<void>}
|
|
441
|
+
*/
|
|
442
|
+
async searchSources() {
|
|
443
|
+
const run = this.sourceRun + 1
|
|
444
|
+
this.sourceRun = run
|
|
445
|
+
this.sourceLoading = true
|
|
446
|
+
const [{ default: axios }, { generateUrl }] = await Promise.all([
|
|
447
|
+
import('@nextcloud/axios'),
|
|
448
|
+
import('@nextcloud/router'),
|
|
449
|
+
])
|
|
450
|
+
const found = await Promise.all(this.activeSources.map(async (source) => {
|
|
451
|
+
const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: source.register, schema: source.schema })
|
|
452
|
+
const limit = Number(source.limit) > 0 ? Math.floor(Number(source.limit)) : 10
|
|
453
|
+
try {
|
|
454
|
+
const response = await axios.get(url, { params: { _search: this.localQuery, _limit: limit } })
|
|
455
|
+
const data = response && response.data ? response.data : {}
|
|
456
|
+
const rows = Array.isArray(data.results) ? data.results : []
|
|
457
|
+
return { source, rows, total: typeof data.total === 'number' ? data.total : rows.length }
|
|
458
|
+
} catch (e) {
|
|
459
|
+
// eslint-disable-next-line no-console
|
|
460
|
+
console.warn('[CnSearchPage] could not search', source.schema, e)
|
|
461
|
+
return { source, rows: [], total: 0 }
|
|
462
|
+
}
|
|
463
|
+
}))
|
|
464
|
+
if (run !== this.sourceRun) {
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
const rows = { ...this.sourceRows }
|
|
468
|
+
const totals = { ...this.sourceTotals }
|
|
469
|
+
for (const { source, rows: list, total } of found) {
|
|
470
|
+
rows[source.key] = list.map((row) => this.resultOf(source, row))
|
|
471
|
+
totals[source.key] = total
|
|
472
|
+
}
|
|
473
|
+
this.sourceRows = rows
|
|
474
|
+
this.sourceTotals = totals
|
|
475
|
+
this.sourceLoading = false
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* One found object as a result row.
|
|
480
|
+
*
|
|
481
|
+
* @param {object} source The list it came from.
|
|
482
|
+
* @param {object} row The object.
|
|
483
|
+
* @return {object} `{ id, title, snippet, schema, route }`.
|
|
484
|
+
*/
|
|
485
|
+
resultOf(source, row) {
|
|
486
|
+
const id = row && (row.id || row['@self']?.id)
|
|
487
|
+
const titleField = source.titleField || ''
|
|
488
|
+
const title = (titleField && row[titleField]) || row.title || row.name || id
|
|
489
|
+
const snippet = source.snippetField ? row[source.snippetField] : undefined
|
|
490
|
+
return {
|
|
491
|
+
id,
|
|
492
|
+
title: typeof title === 'string' ? title : String(title ?? ''),
|
|
493
|
+
...(typeof snippet === 'string' && snippet !== '' ? { snippet } : {}),
|
|
494
|
+
schema: this.shown(source.label) || source.schema,
|
|
495
|
+
route: source.route || '',
|
|
496
|
+
}
|
|
344
497
|
},
|
|
345
498
|
|
|
346
499
|
/**
|
|
@@ -355,6 +508,9 @@ export default {
|
|
|
355
508
|
* @type {object}
|
|
356
509
|
*/
|
|
357
510
|
this.$emit('result-click', result)
|
|
511
|
+
if (this.searchesSources && result && result.route && result.id && this.$router) {
|
|
512
|
+
this.$router.push({ name: result.route, params: { id: result.id } })
|
|
513
|
+
}
|
|
358
514
|
},
|
|
359
515
|
},
|
|
360
516
|
}
|