@conduction/nextcloud-vue 2.102.0 → 2.104.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/CnActionsBar/CnActionsBar.vue2.js +23 -0
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +94 -72
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +18 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -3
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +73 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +99 -64
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +46 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +37 -2
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -6
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +39 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +14 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +40 -2
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +113 -20
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +387 -359
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +173 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +107 -87
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +7 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +10 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue.js +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +57 -5
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +37 -4
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +97 -3
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +46 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +9 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js.map +1 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js +165 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js +85 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -0
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +54 -6
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +3 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +8 -1
- 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/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +18 -2
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +7 -7
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +4 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +4 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/mixins/pageViews.js +8 -1
- package/dist/esm/mixins/pageViews.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +586 -416
- package/dist/esm/utils/nextcloudDirectory.js +18 -1
- package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
- package/dist/esm/utils/sortOrder.js +61 -0
- package/dist/esm/utils/sortOrder.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/visibleWhen.js +13 -2
- package/dist/esm/utils/visibleWhen.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +12270 -10936
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +586 -416
- package/l10n/en.json +4 -0
- package/l10n/nl.json +4 -0
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.md +1 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +39 -2
- package/src/components/CnAppRoot/CnAppRoot.md +1 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +21 -4
- package/src/components/CnConversationThread/CnConversationThread.md +17 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +113 -8
- package/src/components/CnConversationThread/CnConversationWidget.vue +60 -1
- package/src/components/CnDashboardPage/CnDashboardPage.md +4 -1
- package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -6
- package/src/components/CnDataTable/CnDataTable.md +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +49 -0
- package/src/components/CnDetailPage/CnDetailPage.md +2 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +40 -2
- package/src/components/CnFormDialog/CnFormDialog.md +1 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +127 -0
- package/src/components/CnIndexPage/CnIndexPage.md +14 -2
- package/src/components/CnIndexPage/CnIndexPage.vue +192 -11
- package/src/components/CnIndexPage/useSelfFetchList.js +6 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +10 -1
- package/src/components/CnObjectRow/CnObjectRow.vue +95 -1
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +134 -2
- package/src/components/CnSearchSelect/CnSearchSelect.vue +283 -0
- package/src/components/CnSearchSelect/index.js +12 -0
- package/src/components/CnSetupWizard/CnSetupWizard.vue +56 -6
- package/src/components/CnTabsWidget/CnTabsWidget.vue +8 -1
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +7 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +18 -2
- package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
- package/src/components/index.js +1 -0
- package/src/composables/indexSources.js +7 -7
- package/src/css/dialog.css +5 -3
- package/src/css/index-page.css +19 -0
- package/src/css/look-board-index.css +48 -0
- package/src/index.js +1 -0
- package/src/mixins/pageViews.js +8 -1
- package/src/schemas/app-manifest-v2.schema.json +75 -6
- package/src/utils/nextcloudDirectory.js +17 -0
- package/src/utils/sortOrder.js +58 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/visibleWhen.js +13 -2
|
@@ -201,13 +201,38 @@
|
|
|
201
201
|
</span>
|
|
202
202
|
</li>
|
|
203
203
|
</ul>
|
|
204
|
+
<!-- `footerAction`: one action under the list, also under the empty
|
|
205
|
+
text ("Beroep registreren"). With `disabledWhen` it greys out and
|
|
206
|
+
says why (`disabledReason`: "Kan zodra de beslissing is
|
|
207
|
+
verzonden"). -->
|
|
208
|
+
<div
|
|
209
|
+
v-if="footerAction && !error"
|
|
210
|
+
class="cn-record-list__footer"
|
|
211
|
+
data-testid="cn-record-list-footer">
|
|
212
|
+
<NcButton
|
|
213
|
+
variant="secondary"
|
|
214
|
+
:disabled="footerDisabled"
|
|
215
|
+
:aria-describedby="footerDisabled && footerAction.reason ? footerReasonId : null"
|
|
216
|
+
data-testid="cn-record-list-footer-action"
|
|
217
|
+
@click="runFooterAction">
|
|
218
|
+
<template v-if="footerAction.icon" #icon>
|
|
219
|
+
<CnIcon :name="footerAction.icon" :size="16" />
|
|
220
|
+
</template>
|
|
221
|
+
{{ footerAction.label }}
|
|
222
|
+
</NcButton>
|
|
223
|
+
<span
|
|
224
|
+
v-if="footerDisabled && footerAction.reason"
|
|
225
|
+
:id="footerReasonId"
|
|
226
|
+
class="cn-record-list__footer-reason"
|
|
227
|
+
data-testid="cn-record-list-footer-reason">{{ footerAction.reason }}</span>
|
|
228
|
+
</div>
|
|
204
229
|
</div>
|
|
205
230
|
</template>
|
|
206
231
|
|
|
207
232
|
<script>
|
|
208
233
|
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|
209
234
|
import { translate as t } from '@nextcloud/l10n'
|
|
210
|
-
import { NcLoadingIcon } from '@nextcloud/vue'
|
|
235
|
+
import { NcButton, NcLoadingIcon } from '@nextcloud/vue'
|
|
211
236
|
import { inject, ref } from 'vue'
|
|
212
237
|
import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
|
|
213
238
|
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
@@ -224,8 +249,12 @@ import { lensUnavailableText, readLensReports } from '../../utils/lensAvailabili
|
|
|
224
249
|
import { pageFilter, pageSort } from '../../utils/pageControls.js'
|
|
225
250
|
import { readPath } from '../../utils/readPath.js'
|
|
226
251
|
import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
252
|
+
import { evaluateVisibleWhen } from '../../utils/visibleWhen.js'
|
|
227
253
|
|
|
228
254
|
const PAGE_REFRESH_CHANNEL = 'cn:page:refresh'
|
|
255
|
+
|
|
256
|
+
/** Gives each list's footer reason its own element id. */
|
|
257
|
+
let footerSeq = 0
|
|
229
258
|
const PRESENTATIONS = ['rows', 'feed', 'inline', 'cards', 'notice']
|
|
230
259
|
|
|
231
260
|
/** The tones a `notice` list takes. */
|
|
@@ -279,7 +308,7 @@ function unwrap(value) {
|
|
|
279
308
|
export default {
|
|
280
309
|
name: 'CnRecordListWidget',
|
|
281
310
|
|
|
282
|
-
components: { ChevronRight, CnIcon, CnStatusBadge, NcLoadingIcon },
|
|
311
|
+
components: { ChevronRight, CnIcon, CnStatusBadge, NcButton, NcLoadingIcon },
|
|
283
312
|
|
|
284
313
|
inject: {
|
|
285
314
|
/** Detail-page object context (`{ objectId, object, register, schema }`). */
|
|
@@ -314,6 +343,10 @@ export default {
|
|
|
314
343
|
* `presentation: "notice"` draws the rows as sentences in one box toned by
|
|
315
344
|
* `variant` (`info`, `warning` or `error`), after an optional `lead`
|
|
316
345
|
* (`{total}` filled in).
|
|
346
|
+
* `footerAction` (`{ label, icon?, type, ..., disabledWhen?,
|
|
347
|
+
* disabledReason? }`) ends the card with one action, also under the
|
|
348
|
+
* empty text; while the visibleWhen condition `disabledWhen` holds the
|
|
349
|
+
* button is disabled and `disabledReason` shows under it.
|
|
317
350
|
*
|
|
318
351
|
* @type {object}
|
|
319
352
|
*/
|
|
@@ -367,10 +400,48 @@ export default {
|
|
|
367
400
|
error: false,
|
|
368
401
|
/** Personal-lens reports (`@self.lenses`) of the latest response. */
|
|
369
402
|
lenses: {},
|
|
403
|
+
/** Whether `footerAction.disabledWhen` holds. */
|
|
404
|
+
footerDisabled: false,
|
|
405
|
+
footerReasonId: `cn-record-list-footer-reason-${++footerSeq}`,
|
|
370
406
|
}
|
|
371
407
|
},
|
|
372
408
|
|
|
373
409
|
computed: {
|
|
410
|
+
/**
|
|
411
|
+
* The action under the list (`footerAction`: `{ label, icon?, type,
|
|
412
|
+
* ..., disabledWhen?, disabledReason? }`), labels through the host
|
|
413
|
+
* translate, or null without a label.
|
|
414
|
+
*
|
|
415
|
+
* @spec openspec/changes/record-list-footer-action/specs/record-list/spec.md#requirement-a-record-list-can-end-with-an-action-that-says-why-it-is-disabled
|
|
416
|
+
* @return {{label: string, icon: string, reason: string, action: object}|null}
|
|
417
|
+
*/
|
|
418
|
+
footerAction() {
|
|
419
|
+
const action = this.content?.footerAction
|
|
420
|
+
if (!action || typeof action !== 'object' || typeof action.label !== 'string' || action.label === '') {
|
|
421
|
+
return null
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
label: this.translate(action.label),
|
|
425
|
+
icon: typeof action.icon === 'string' ? action.icon : '',
|
|
426
|
+
reason: typeof action.disabledReason === 'string' && action.disabledReason !== '' ? this.translate(action.disabledReason) : '',
|
|
427
|
+
action,
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* What `footerAction.disabledWhen` depends on, so it is evaluated again
|
|
433
|
+
* when the condition, the record or the workspace changes.
|
|
434
|
+
*
|
|
435
|
+
* @return {string}
|
|
436
|
+
*/
|
|
437
|
+
footerDisabledKey() {
|
|
438
|
+
const cond = this.content?.footerAction?.disabledWhen
|
|
439
|
+
if (!cond) {
|
|
440
|
+
return ''
|
|
441
|
+
}
|
|
442
|
+
return JSON.stringify({ cond, object: this.tokenCtx.object, workspace: this.tokenCtx.workspace, total: this.total })
|
|
443
|
+
},
|
|
444
|
+
|
|
374
445
|
/** @return {string} `rows`, `feed`, `inline` or `cards`. */
|
|
375
446
|
/**
|
|
376
447
|
* The tone of a `notice` list (`content.variant`): info, warning or error.
|
|
@@ -595,6 +666,13 @@ export default {
|
|
|
595
666
|
this.fetchRows()
|
|
596
667
|
},
|
|
597
668
|
|
|
669
|
+
footerDisabledKey: {
|
|
670
|
+
immediate: true,
|
|
671
|
+
handler() {
|
|
672
|
+
this.evaluateFooterDisabled()
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
|
|
598
676
|
endpointKey() {
|
|
599
677
|
this.fetchRows()
|
|
600
678
|
},
|
|
@@ -621,6 +699,47 @@ export default {
|
|
|
621
699
|
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
622
700
|
},
|
|
623
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Settle whether the footer action is disabled: `disabledWhen` is a
|
|
704
|
+
* visibleWhen condition read against the page's record (`field`), the
|
|
705
|
+
* workspace, an endpoint or a source; `total` is the list's total.
|
|
706
|
+
*
|
|
707
|
+
* @spec openspec/changes/record-list-footer-action/specs/record-list/spec.md#requirement-a-record-list-can-end-with-an-action-that-says-why-it-is-disabled
|
|
708
|
+
* @return {Promise<void>}
|
|
709
|
+
*/
|
|
710
|
+
async evaluateFooterDisabled() {
|
|
711
|
+
const cond = this.content?.footerAction?.disabledWhen
|
|
712
|
+
if (!cond) {
|
|
713
|
+
this.footerDisabled = false
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
const ctx = { ...this.tokenCtx, object: { ...(this.tokenCtx.object || {}), total: this.total } }
|
|
717
|
+
const key = this.footerDisabledKey
|
|
718
|
+
const disabled = await evaluateVisibleWhen(cond, ctx)
|
|
719
|
+
if (key === this.footerDisabledKey) {
|
|
720
|
+
this.footerDisabled = disabled
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Run the footer action through the page's dispatcher, with the page's
|
|
726
|
+
* record as the token context.
|
|
727
|
+
*
|
|
728
|
+
* @spec openspec/changes/record-list-footer-action/specs/record-list/spec.md#requirement-a-record-list-can-end-with-an-action-that-says-why-it-is-disabled
|
|
729
|
+
* @return {unknown} What the dispatcher returns.
|
|
730
|
+
*/
|
|
731
|
+
runFooterAction() {
|
|
732
|
+
const declared = this.footerAction?.action
|
|
733
|
+
if (!declared || this.footerDisabled) {
|
|
734
|
+
return undefined
|
|
735
|
+
}
|
|
736
|
+
const { disabledWhen, disabledReason, ...action } = declared
|
|
737
|
+
if (typeof this.cnDispatchAction === 'function') {
|
|
738
|
+
return this.cnDispatchAction(action)
|
|
739
|
+
}
|
|
740
|
+
return dispatchAction(action, { router: this.$router || null, tokenCtx: this.tokenCtx, source: { register: this.content?.register, schema: this.content?.schema } })
|
|
741
|
+
},
|
|
742
|
+
|
|
624
743
|
/**
|
|
625
744
|
* A row's `bar`: its value, the maximum it is measured against (the
|
|
626
745
|
* row's `maxField`, else the largest row), the filled share and
|
|
@@ -1196,6 +1315,19 @@ export default {
|
|
|
1196
1315
|
padding: 0;
|
|
1197
1316
|
}
|
|
1198
1317
|
|
|
1318
|
+
.cn-record-list__footer {
|
|
1319
|
+
display: flex;
|
|
1320
|
+
flex-direction: column;
|
|
1321
|
+
align-items: flex-start;
|
|
1322
|
+
gap: 6px;
|
|
1323
|
+
margin-top: 10px;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
.cn-record-list__footer-reason {
|
|
1327
|
+
font-size: 13px;
|
|
1328
|
+
color: var(--color-text-maxcontrast);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1199
1331
|
.cn-record-list__empty,
|
|
1200
1332
|
.cn-record-list__error {
|
|
1201
1333
|
color: var(--color-text-maxcontrast);
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<div class="cn-search-select" :class="{ 'cn-search-select--label-outside': labelOutside }" data-testid="cn-search-select">
|
|
7
|
+
<NcSelect
|
|
8
|
+
:inputId="inputId"
|
|
9
|
+
:inputLabel="inputLabel"
|
|
10
|
+
:labelOutside="labelOutside"
|
|
11
|
+
:modelValue="selectedOption"
|
|
12
|
+
:options="options"
|
|
13
|
+
:loading="loading"
|
|
14
|
+
:clearable="clearable"
|
|
15
|
+
:disabled="disabled"
|
|
16
|
+
:placeholder="resolvedPlaceholder"
|
|
17
|
+
:filterable="filterable"
|
|
18
|
+
label="label"
|
|
19
|
+
@search="onSearch"
|
|
20
|
+
@update:modelValue="onInput">
|
|
21
|
+
<template #option="option">
|
|
22
|
+
<span class="cn-search-select__row" data-testid="cn-search-select-row">
|
|
23
|
+
<span
|
|
24
|
+
v-if="option.avatar || option.icon"
|
|
25
|
+
class="cn-search-select__lead"
|
|
26
|
+
:class="{ 'cn-search-select__lead--avatar': option.avatar && !option.icon }"
|
|
27
|
+
aria-hidden="true">
|
|
28
|
+
<CnIcon v-if="option.icon" :name="option.icon" :size="18" />
|
|
29
|
+
<template v-else>{{ initialsOf(option) }}</template>
|
|
30
|
+
</span>
|
|
31
|
+
<span class="cn-search-select__text">
|
|
32
|
+
<span class="cn-search-select__name">{{ option.label }}</span>
|
|
33
|
+
<span v-if="option.secondary" class="cn-search-select__secondary">{{ option.secondary }}</span>
|
|
34
|
+
</span>
|
|
35
|
+
<Check
|
|
36
|
+
v-if="isSelected(option)"
|
|
37
|
+
class="cn-search-select__check"
|
|
38
|
+
:size="18"
|
|
39
|
+
data-testid="cn-search-select-check" />
|
|
40
|
+
</span>
|
|
41
|
+
</template>
|
|
42
|
+
</NcSelect>
|
|
43
|
+
<Magnify class="cn-search-select__magnifier" :size="18" aria-hidden="true" />
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script>
|
|
48
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
49
|
+
import { NcSelect } from '@nextcloud/vue'
|
|
50
|
+
import Check from 'vue-material-design-icons/Check.vue'
|
|
51
|
+
import Magnify from 'vue-material-design-icons/Magnify.vue'
|
|
52
|
+
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
53
|
+
import { initialsOf } from '../../utils/initials.js'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* CnSearchSelect — a single picker drawn as a search field: a magnifier
|
|
57
|
+
* instead of the chevron, and results as rows with an icon or initials, a
|
|
58
|
+
* name, a secondary line and a check on the chosen one (boards DqOverdragen
|
|
59
|
+
* "Nieuw team", DqNieuweZaak "Aanvrager").
|
|
60
|
+
*
|
|
61
|
+
* Each option is `{ id, label, secondary?, icon?, avatar? }`: `icon` names a
|
|
62
|
+
* CnIcon, `avatar: true` draws the label's initials in a circle. The value is
|
|
63
|
+
* the option's `id` (or `value`); `update:modelValue` emits that id, or null
|
|
64
|
+
* when cleared. Searching is the host's: listen to `search` and pass new
|
|
65
|
+
* `options`, with `filterable: false` when the host already filtered them.
|
|
66
|
+
*
|
|
67
|
+
* ```vue
|
|
68
|
+
* <CnSearchSelect
|
|
69
|
+
* v-model="team"
|
|
70
|
+
* inputLabel="Nieuw team"
|
|
71
|
+
* :options="[{ id: 'legal', label: 'Juridische zaken', secondary: '6 leden · bezwaar, beroep en Woo-advies', avatar: true }]" />
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export default {
|
|
75
|
+
name: 'CnSearchSelect',
|
|
76
|
+
|
|
77
|
+
components: { Check, CnIcon, Magnify, NcSelect },
|
|
78
|
+
|
|
79
|
+
props: {
|
|
80
|
+
/** The chosen option's id, or the option itself. */
|
|
81
|
+
modelValue: {
|
|
82
|
+
type: [String, Number, Object],
|
|
83
|
+
default: null,
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The options: `{ id, label, secondary?, icon?, avatar? }`.
|
|
88
|
+
*
|
|
89
|
+
* @type {Array<{id: (string|number), label: string, secondary?: string, icon?: string, avatar?: boolean}>}
|
|
90
|
+
*/
|
|
91
|
+
options: {
|
|
92
|
+
type: Array,
|
|
93
|
+
default: () => [],
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/** The field's label. */
|
|
97
|
+
inputLabel: {
|
|
98
|
+
type: String,
|
|
99
|
+
default: '',
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/** Draw the label above the field instead of inside it. */
|
|
103
|
+
labelOutside: {
|
|
104
|
+
type: Boolean,
|
|
105
|
+
default: false,
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/** The input's id. */
|
|
109
|
+
inputId: {
|
|
110
|
+
type: String,
|
|
111
|
+
default: undefined,
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/** Placeholder text; defaults to "Search…". */
|
|
115
|
+
placeholder: {
|
|
116
|
+
type: String,
|
|
117
|
+
default: '',
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/** Show the loading spinner. */
|
|
121
|
+
loading: {
|
|
122
|
+
type: Boolean,
|
|
123
|
+
default: false,
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/** Offer a clear button. */
|
|
127
|
+
clearable: {
|
|
128
|
+
type: Boolean,
|
|
129
|
+
default: true,
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/** Disable the field. */
|
|
133
|
+
disabled: {
|
|
134
|
+
type: Boolean,
|
|
135
|
+
default: false,
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/** Filter the options on the typed text; false when the host searches. */
|
|
139
|
+
filterable: {
|
|
140
|
+
type: Boolean,
|
|
141
|
+
default: true,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
emits: ['update:modelValue', 'search'],
|
|
146
|
+
|
|
147
|
+
computed: {
|
|
148
|
+
/**
|
|
149
|
+
* The option matching `modelValue`, or a bare one carrying it.
|
|
150
|
+
*
|
|
151
|
+
* @return {object|null}
|
|
152
|
+
*/
|
|
153
|
+
selectedOption() {
|
|
154
|
+
const value = this.modelValue
|
|
155
|
+
if (value === null || value === undefined || value === '') {
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
if (typeof value === 'object') {
|
|
159
|
+
return value
|
|
160
|
+
}
|
|
161
|
+
return this.options.find((o) => this.idOf(o) === String(value)) || { id: value, label: String(value) }
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/** @return {string} The placeholder. */
|
|
165
|
+
resolvedPlaceholder() {
|
|
166
|
+
return this.placeholder || t('nextcloud-vue', 'Search…')
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
methods: {
|
|
171
|
+
initialsOf(option) {
|
|
172
|
+
return initialsOf(option.label || '')
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {object} option An option.
|
|
177
|
+
* @return {string} Its id as a string, '' without one.
|
|
178
|
+
*/
|
|
179
|
+
idOf(option) {
|
|
180
|
+
const id = option && (option.id ?? option.value)
|
|
181
|
+
return id === null || id === undefined ? '' : String(id)
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {object} option An option.
|
|
186
|
+
* @return {boolean} Whether it is the chosen one.
|
|
187
|
+
*/
|
|
188
|
+
isSelected(option) {
|
|
189
|
+
return this.selectedOption !== null && this.idOf(option) !== '' && this.idOf(option) === this.idOf(this.selectedOption)
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {object|null} option The picked option.
|
|
194
|
+
*/
|
|
195
|
+
onInput(option) {
|
|
196
|
+
const id = option ? (option.id ?? option.value ?? null) : null
|
|
197
|
+
/** @event update:modelValue The picked option's id, or null when cleared. */
|
|
198
|
+
this.$emit('update:modelValue', id)
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {string} query The typed text.
|
|
203
|
+
*/
|
|
204
|
+
onSearch(query) {
|
|
205
|
+
/** @event search The typed text, for a host that searches itself. */
|
|
206
|
+
this.$emit('search', query)
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
</script>
|
|
211
|
+
|
|
212
|
+
<style scoped>
|
|
213
|
+
.cn-search-select {
|
|
214
|
+
position: relative;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* The magnifier sits where the chevron was: in the field, at its end. */
|
|
218
|
+
.cn-search-select__magnifier {
|
|
219
|
+
position: absolute;
|
|
220
|
+
inset-inline-end: 12px;
|
|
221
|
+
bottom: 0;
|
|
222
|
+
height: var(--default-clickable-area, 34px);
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
color: var(--color-text-maxcontrast);
|
|
226
|
+
pointer-events: none;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.cn-search-select :deep(.vs__open-indicator),
|
|
230
|
+
.cn-search-select :deep(.vs__open-indicator-button) {
|
|
231
|
+
display: none;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.cn-search-select :deep(.vs__actions) {
|
|
235
|
+
padding-inline-end: 36px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.cn-search-select__row {
|
|
239
|
+
display: flex;
|
|
240
|
+
align-items: center;
|
|
241
|
+
gap: 10px;
|
|
242
|
+
width: 100%;
|
|
243
|
+
min-width: 0;
|
|
244
|
+
padding: 4px 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.cn-search-select__lead {
|
|
248
|
+
flex: 0 0 32px;
|
|
249
|
+
width: 32px;
|
|
250
|
+
height: 32px;
|
|
251
|
+
border-radius: 50%;
|
|
252
|
+
display: inline-flex;
|
|
253
|
+
align-items: center;
|
|
254
|
+
justify-content: center;
|
|
255
|
+
background: var(--color-primary-element-light);
|
|
256
|
+
color: var(--color-primary-element-light-text);
|
|
257
|
+
font-size: 13px;
|
|
258
|
+
font-weight: 600;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.cn-search-select__text {
|
|
262
|
+
display: flex;
|
|
263
|
+
flex-direction: column;
|
|
264
|
+
flex: 1 1 auto;
|
|
265
|
+
min-width: 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.cn-search-select__name {
|
|
269
|
+
font-weight: 600;
|
|
270
|
+
overflow-wrap: anywhere;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.cn-search-select__secondary {
|
|
274
|
+
font-size: 13px;
|
|
275
|
+
color: var(--color-text-maxcontrast);
|
|
276
|
+
overflow-wrap: anywhere;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.cn-search-select__check {
|
|
280
|
+
flex: 0 0 auto;
|
|
281
|
+
color: var(--color-primary-element);
|
|
282
|
+
}
|
|
283
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnSearchSelect — a single picker drawn as a search field with rows that
|
|
6
|
+
* carry an icon, a name, a secondary line and a check on the chosen one.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import CnSearchSelect from './CnSearchSelect.vue'
|
|
10
|
+
|
|
11
|
+
export { CnSearchSelect }
|
|
12
|
+
export default CnSearchSelect
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
ref="wizard"
|
|
6
6
|
:steps="wizardSteps"
|
|
7
7
|
:dialogTitle="resolvedDialogTitle"
|
|
8
|
+
stepHeading
|
|
8
9
|
:submitLabel="submitLabel"
|
|
9
10
|
:cancelLabel="cancelLabel"
|
|
10
11
|
:nextLabel="nextLabel"
|
|
@@ -79,6 +80,7 @@
|
|
|
79
80
|
</NcNoteCard>
|
|
80
81
|
<CnChoiceCards
|
|
81
82
|
v-if="isCardChoice(step)"
|
|
83
|
+
:layout="step.display === 'rows' ? 'rows' : 'cards'"
|
|
82
84
|
:label="stepTitle(step)"
|
|
83
85
|
:options="optionsFor(step)"
|
|
84
86
|
:multiple="step.multiple === true"
|
|
@@ -464,14 +466,16 @@ export default {
|
|
|
464
466
|
computed: {
|
|
465
467
|
/**
|
|
466
468
|
* The title actually rendered by CnWizardDialog: "Set up {appName}"
|
|
467
|
-
* when a display name was passed
|
|
468
|
-
* resolves to (a caller
|
|
469
|
+
* when a display name was passed (the name through the host's
|
|
470
|
+
* translate), otherwise whatever `dialogTitle` resolves to (a caller
|
|
471
|
+
* override, or its own generic default). In the board look it is the
|
|
472
|
+
* eyebrow's context and the current step's title is the heading.
|
|
469
473
|
*
|
|
470
474
|
* @return {string}
|
|
471
475
|
*/
|
|
472
476
|
resolvedDialogTitle() {
|
|
473
477
|
return this.appName
|
|
474
|
-
? t('nextcloud-vue', 'Set up {app}', { app: this.appName })
|
|
478
|
+
? t('nextcloud-vue', 'Set up {app}', { app: this.cnTranslate(this.appName) })
|
|
475
479
|
: this.dialogTitle
|
|
476
480
|
},
|
|
477
481
|
|
|
@@ -727,6 +731,12 @@ export default {
|
|
|
727
731
|
},
|
|
728
732
|
},
|
|
729
733
|
|
|
734
|
+
created() {
|
|
735
|
+
for (const step of this.setupSteps) {
|
|
736
|
+
this.applyDefault(step)
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
|
|
730
740
|
mounted() {
|
|
731
741
|
// Covers a RESUMED wizard opening straight onto its run-action step
|
|
732
742
|
// (`initialStepId`) — `onStepChange` only fires on later navigation,
|
|
@@ -944,13 +954,14 @@ export default {
|
|
|
944
954
|
},
|
|
945
955
|
|
|
946
956
|
/**
|
|
947
|
-
* Whether a choice step renders as a card grid
|
|
957
|
+
* Whether a choice step renders as CnChoiceCards (a card grid, or radio
|
|
958
|
+
* rows with `display: "rows"`) rather than a dropdown.
|
|
948
959
|
*
|
|
949
960
|
* @param {object} step The choice step.
|
|
950
|
-
* @return {boolean} True for `display: "cards"`.
|
|
961
|
+
* @return {boolean} True for `display: "cards"` and `display: "rows"`.
|
|
951
962
|
*/
|
|
952
963
|
isCardChoice(step) {
|
|
953
|
-
return step.display === 'cards'
|
|
964
|
+
return step.display === 'cards' || step.display === 'rows'
|
|
954
965
|
},
|
|
955
966
|
|
|
956
967
|
/**
|
|
@@ -1054,6 +1065,36 @@ export default {
|
|
|
1054
1065
|
}
|
|
1055
1066
|
},
|
|
1056
1067
|
|
|
1068
|
+
/**
|
|
1069
|
+
* Preselect a `choice` step's `default` option. Skipped once the user
|
|
1070
|
+
* picked here, when a value is already chosen, and for a step the
|
|
1071
|
+
* server already completed (its stored value must not be overwritten
|
|
1072
|
+
* by a Next). A card step holds the plain value; a dropdown holds the
|
|
1073
|
+
* option object, or the value while the options are still loading.
|
|
1074
|
+
*
|
|
1075
|
+
* @param {object} step The choice step.
|
|
1076
|
+
* @return {void}
|
|
1077
|
+
*/
|
|
1078
|
+
applyDefault(step) {
|
|
1079
|
+
if (!step || step.type !== 'choice' || step.default === undefined || step.default === null) {
|
|
1080
|
+
return
|
|
1081
|
+
}
|
|
1082
|
+
if (this.userTouched[step.id] === true || this.hasChoice(step) || this.isServerDone(step.id)) {
|
|
1083
|
+
return
|
|
1084
|
+
}
|
|
1085
|
+
const wanted = step.multiple === true ? [].concat(step.default) : step.default
|
|
1086
|
+
if (this.isCardChoice(step)) {
|
|
1087
|
+
this.choiceModel[step.id] = wanted
|
|
1088
|
+
} else {
|
|
1089
|
+
const options = this.optionsFor(step)
|
|
1090
|
+
const pick = (value) => options.find((o) => o.value === value) || value
|
|
1091
|
+
this.choiceModel[step.id] = Array.isArray(wanted) ? wanted.map(pick) : pick(wanted)
|
|
1092
|
+
}
|
|
1093
|
+
if (step.configKey) {
|
|
1094
|
+
this.choiceValues[step.configKey] = this.scalarChoice(step)
|
|
1095
|
+
}
|
|
1096
|
+
},
|
|
1097
|
+
|
|
1057
1098
|
/**
|
|
1058
1099
|
* Auto-fill a `choice` step's suggested default from an earlier choice
|
|
1059
1100
|
* (its `suggestFrom` configKey → `suggestMap[parentValue]`). No-op once
|
|
@@ -1360,6 +1401,15 @@ export default {
|
|
|
1360
1401
|
if (step && step.suggestFrom) {
|
|
1361
1402
|
this.applySuggestion(step)
|
|
1362
1403
|
}
|
|
1404
|
+
// A default seeded before a dropdown's options arrived is a bare
|
|
1405
|
+
// value; swap in its option object now the step opens.
|
|
1406
|
+
if (step && step.default !== undefined && this.userTouched[step.id] !== true && !this.isCardChoice(step)) {
|
|
1407
|
+
const held = this.choiceModel[step.id]
|
|
1408
|
+
const option = this.optionsFor(step).find((o) => o.value === held)
|
|
1409
|
+
if (option && held !== null && typeof held !== 'object') {
|
|
1410
|
+
this.choiceModel[step.id] = option
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1363
1413
|
this.maybeAutoRunStep(payload.stepId)
|
|
1364
1414
|
/**
|
|
1365
1415
|
* @event step-change Emitted when the active step changes.
|
|
@@ -624,6 +624,7 @@ export default {
|
|
|
624
624
|
// is already going under "More" does not use one up.
|
|
625
625
|
let placed = pinActivity ? 1 : 0
|
|
626
626
|
const resolved = []
|
|
627
|
+
const usedKeys = new Set()
|
|
627
628
|
tabs.forEach((tab, position) => {
|
|
628
629
|
const index = originalIndex[position]
|
|
629
630
|
// A tab whose condition is false is absent, not empty.
|
|
@@ -654,8 +655,14 @@ export default {
|
|
|
654
655
|
placed += 1
|
|
655
656
|
}
|
|
656
657
|
}
|
|
658
|
+
// Keyed on the tab's own id, else its place in `content.tabs`:
|
|
659
|
+
// never on the widget the panel starts with, which changes when
|
|
660
|
+
// a stack's first card turns visible and would remount the panel.
|
|
661
|
+
const ownId = tab && typeof tab === 'object' && typeof tab.id === 'string' ? tab.id : ''
|
|
662
|
+
const key = ownId !== '' && !usedKeys.has(ownId) ? ownId : `tab-${index}`
|
|
663
|
+
usedKeys.add(key)
|
|
657
664
|
resolved.push({
|
|
658
|
-
key
|
|
665
|
+
key,
|
|
659
666
|
id: (tab && typeof tab === 'object' && tab.id) || widgetId || '',
|
|
660
667
|
widgetId,
|
|
661
668
|
label: this.tabLabel(tab, widget, widgetId),
|
|
@@ -339,7 +339,11 @@ export default {
|
|
|
339
339
|
list-style: none;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
|
|
342
|
+
/* A navigation entry, not a link in running text: no underline, also not
|
|
343
|
+
on hover. `!important` because a theme's `a { text-decoration }` carries
|
|
344
|
+
it (thematiq), and a bare `a` with `!important` beats any class. */
|
|
345
|
+
.cn-user-settings-dialog__nav-link,
|
|
346
|
+
.cn-user-settings-dialog__nav-link:visited {
|
|
343
347
|
display: flex;
|
|
344
348
|
align-items: center;
|
|
345
349
|
gap: 10px;
|
|
@@ -348,11 +352,12 @@ export default {
|
|
|
348
352
|
border-radius: var(--border-radius-element, 8px);
|
|
349
353
|
font-size: 15px;
|
|
350
354
|
color: var(--color-main-text);
|
|
351
|
-
text-decoration: none;
|
|
355
|
+
text-decoration: none !important;
|
|
352
356
|
}
|
|
353
357
|
|
|
354
358
|
.cn-user-settings-dialog__nav-link:hover {
|
|
355
359
|
background: var(--color-background-hover);
|
|
360
|
+
text-decoration: none !important;
|
|
356
361
|
}
|
|
357
362
|
|
|
358
363
|
.cn-user-settings-dialog__nav-link:focus-visible {
|