@conduction/nextcloud-vue 2.62.0 → 2.64.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/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +425 -39
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +119 -42
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +186 -9
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +65 -42
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +61 -16
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +20 -0
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -2
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +152 -135
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +253 -9
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +126 -50
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +66 -14
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +23 -14
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +81 -4
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +22 -5
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +81 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +7 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
- package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +41 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +8 -3
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +63 -2
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +96 -37
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/stagesModel.js +5 -2
- package/dist/esm/components/CnStagesWidget/stagesModel.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +52 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -6
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +14 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +2 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
- package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +73 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +14 -2
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +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/useDependencyCheck.js +65 -0
- package/dist/esm/composables/useDependencyCheck.js.map +1 -0
- package/dist/esm/composables/useWalkthrough.js +218 -6
- package/dist/esm/composables/useWalkthrough.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 +45 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +45 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +780 -81
- package/dist/esm/store/useObjectStore.js +8 -0
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/columnFilters.js +311 -0
- package/dist/esm/utils/columnFilters.js.map +1 -0
- package/dist/esm/utils/groupAutocomplete.js +123 -0
- package/dist/esm/utils/groupAutocomplete.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +2 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/pickerOptions.js +198 -0
- package/dist/esm/utils/pickerOptions.js.map +1 -0
- package/dist/esm/utils/schema.js +83 -7
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/schemaRefSlug.js +19 -3
- package/dist/esm/utils/schemaRefSlug.js.map +1 -1
- package/dist/esm/utils/timelineEvents.js +174 -0
- package/dist/esm/utils/timelineEvents.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/walkthroughSignals.js +88 -0
- package/dist/esm/utils/walkthroughSignals.js.map +1 -0
- package/dist/nextcloud-vue.cjs.js +13088 -7937
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +780 -81
- package/l10n/en.json +45 -0
- package/l10n/nl.json +45 -0
- package/package.json +1 -1
- package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
- package/src/components/CnAdminActionCard/index.js +4 -0
- package/src/components/CnAppNav/CnAppNav.vue +568 -43
- package/src/components/CnAppRoot/CnAppRoot.md +4 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +223 -16
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +61 -16
- package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +22 -1
- package/src/components/CnCellRenderer/CnCellRenderer.vue +33 -2
- package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
- package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -4
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
- package/src/components/CnDataTable/CnDataTable.md +3 -0
- package/src/components/CnDataTable/CnDataTable.vue +305 -5
- package/src/components/CnDetailPage/CnDetailPage.md +4 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +60 -1
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
- package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
- package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +116 -6
- package/src/components/CnIndexPage/CnIndexPage.vue +87 -4
- package/src/components/CnIndexPage/selfModeActions.js +3 -3
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +46 -2
- package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
- package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
- package/src/components/CnStagesWidget/CnStagesWidget.vue +180 -2
- package/src/components/CnStagesWidget/stagesModel.js +5 -2
- package/src/components/CnStatWidget/CnStatWidget.vue +57 -3
- package/src/components/CnTabsWidget/CnTabsWidget.vue +15 -2
- package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
- package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
- package/src/components/CnTimelineWidget/index.js +4 -0
- package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
- package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +4 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +107 -1
- package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
- package/src/components/index.js +2 -0
- package/src/composables/useDependencyCheck.js +61 -0
- package/src/composables/useWalkthrough.js +217 -5
- package/src/css/dashboard.css +49 -0
- package/src/css/detail-page.css +29 -4
- package/src/css/kpi-card.css +24 -1
- package/src/css/table.css +99 -6
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +135 -3
- package/src/store/useObjectStore.js +8 -0
- package/src/utils/columnFilters.js +324 -0
- package/src/utils/groupAutocomplete.js +119 -0
- package/src/utils/libraryWidgetKeys.js +2 -0
- package/src/utils/pickerOptions.js +194 -0
- package/src/utils/schema.js +83 -7
- package/src/utils/schemaRefSlug.js +19 -3
- package/src/utils/timelineEvents.js +171 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
- package/src/utils/walkthroughSignals.js +94 -0
|
@@ -179,16 +179,20 @@
|
|
|
179
179
|
references get CnResourceSelect; plain references keep the
|
|
180
180
|
read-only NcSelect below. -->
|
|
181
181
|
<div
|
|
182
|
-
v-else-if="isReferenceField(field) && field.allowCreate"
|
|
182
|
+
v-else-if="(isReferenceField(field) || isReferenceArrayField(field)) && field.allowCreate"
|
|
183
183
|
class="cn-form-dialog__select-wrapper">
|
|
184
184
|
<CnResourceSelect
|
|
185
185
|
:inputId="'cn-form-' + field.key"
|
|
186
186
|
:inputLabel="field.label + (field.required ? ' *' : '')"
|
|
187
187
|
:register="referenceRegister(field)"
|
|
188
|
-
:schema="field.reference.schema"
|
|
188
|
+
:schema="String(field.reference.schema)"
|
|
189
189
|
:labelField="referenceLabelField(field)"
|
|
190
|
-
:
|
|
191
|
-
:
|
|
190
|
+
:multiple="!!field.reference.multiple"
|
|
191
|
+
:modelValue="referenceModelValue(field)"
|
|
192
|
+
:clearable="!field.required || !!field.reference.multiple"
|
|
193
|
+
:disabled="field.readOnly"
|
|
194
|
+
:preload="true"
|
|
195
|
+
:createHandler="term => openNestedCreate(field, term)"
|
|
192
196
|
@update:modelValue="value => onReferenceSelected(field, value)"
|
|
193
197
|
@create="obj => onReferenceCreated(field, obj)" />
|
|
194
198
|
<CnFieldHelper
|
|
@@ -199,14 +203,19 @@
|
|
|
199
203
|
|
|
200
204
|
<!-- Enum toggle (`widget: "switch"` on a 2-value enum):
|
|
201
205
|
renders a switch mapping off→enum[0], on→last enum value. -->
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
<div v-else-if="field.widget === 'switch'" class="cn-form-dialog__switch-wrapper">
|
|
207
|
+
<NcCheckboxRadioSwitch
|
|
208
|
+
:modelValue="isSwitchOn(field)"
|
|
209
|
+
:disabled="field.readOnly"
|
|
210
|
+
type="switch"
|
|
211
|
+
@update:modelValue="value => updateField(field.key, switchValueFor(field, value))">
|
|
212
|
+
{{ field.label }}{{ field.required ? ' *' : '' }}
|
|
213
|
+
</NcCheckboxRadioSwitch>
|
|
214
|
+
<CnFieldHelper
|
|
215
|
+
:text="field.description"
|
|
216
|
+
:more="field.descriptionLong"
|
|
217
|
+
:error="errors[field.key]" />
|
|
218
|
+
</div>
|
|
210
219
|
|
|
211
220
|
<!-- Select (enum / $ref object reference / single Nextcloud user, supports async function).
|
|
212
221
|
A single Nextcloud-user field (`widget: "user"`) renders here too:
|
|
@@ -214,7 +223,7 @@
|
|
|
214
223
|
user picker, and the async load/search/select machinery (getEffective*,
|
|
215
224
|
onAsyncSearch) resolves real users — mirroring how `user-multiselect`
|
|
216
225
|
shares the multiselect branch below. -->
|
|
217
|
-
<div v-else-if="field.widget === 'select' || field.widget === 'user'" class="cn-form-dialog__select-wrapper">
|
|
226
|
+
<div v-else-if="field.widget === 'select' || field.widget === 'user' || field.widget === 'group'" class="cn-form-dialog__select-wrapper">
|
|
218
227
|
<!-- A `widget: "user"` field renders NcSelectUsers, everything
|
|
219
228
|
else NcSelect. It used to be one NcSelect with
|
|
220
229
|
`:user-select="isUserField(field)"`, and @nextcloud/vue 9
|
|
@@ -256,7 +265,7 @@
|
|
|
256
265
|
</div>
|
|
257
266
|
|
|
258
267
|
<!-- Multiselect (array enum items / $ref array / Nextcloud users, supports async function) -->
|
|
259
|
-
<div v-else-if="field.widget === 'multiselect' || field.widget === 'user-multiselect'" class="cn-form-dialog__select-wrapper">
|
|
268
|
+
<div v-else-if="field.widget === 'multiselect' || field.widget === 'user-multiselect' || field.widget === 'group-multiselect'" class="cn-form-dialog__select-wrapper">
|
|
260
269
|
<NcSelect
|
|
261
270
|
:inputId="'cn-form-' + field.key"
|
|
262
271
|
:inputLabel="field.label + (field.required ? ' *' : '')"
|
|
@@ -322,14 +331,19 @@
|
|
|
322
331
|
</div>
|
|
323
332
|
|
|
324
333
|
<!-- Checkbox / Switch (boolean) -->
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
334
|
+
<div v-else-if="field.widget === 'checkbox'" class="cn-form-dialog__switch-wrapper">
|
|
335
|
+
<NcCheckboxRadioSwitch
|
|
336
|
+
:modelValue="!!formData[field.key]"
|
|
337
|
+
:disabled="field.readOnly"
|
|
338
|
+
type="switch"
|
|
339
|
+
@update:modelValue="value => updateField(field.key, value)">
|
|
340
|
+
{{ field.label }}{{ field.required ? ' *' : '' }}
|
|
341
|
+
</NcCheckboxRadioSwitch>
|
|
342
|
+
<CnFieldHelper
|
|
343
|
+
:text="field.description"
|
|
344
|
+
:more="field.descriptionLong"
|
|
345
|
+
:error="errors[field.key]" />
|
|
346
|
+
</div>
|
|
333
347
|
|
|
334
348
|
<!-- Date / Datetime (NcTextField's type validator rejects
|
|
335
349
|
'date'/'datetime-local', so use NcDateTimePickerNative) -->
|
|
@@ -438,6 +452,21 @@
|
|
|
438
452
|
</template>
|
|
439
453
|
</div>
|
|
440
454
|
|
|
455
|
+
<!-- "Create" on a select-or-create reference (`x-allow-create`) opens
|
|
456
|
+
the referenced schema's own form on top of this one. Saving it
|
|
457
|
+
selects the new object in the field that asked for it. -->
|
|
458
|
+
<CnFormDialog
|
|
459
|
+
v-if="nestedCreate !== null"
|
|
460
|
+
ref="nestedCreateDialog"
|
|
461
|
+
:schema="nestedCreate.schema"
|
|
462
|
+
:register="nestedCreate.register"
|
|
463
|
+
:item="null"
|
|
464
|
+
:initialData="nestedCreate.initialData"
|
|
465
|
+
:recoverDraft="false"
|
|
466
|
+
data-testid="cn-form-dialog-nested-create"
|
|
467
|
+
@confirm="onNestedCreateConfirm"
|
|
468
|
+
@close="onNestedCreateClose" />
|
|
469
|
+
|
|
441
470
|
<template #actions>
|
|
442
471
|
<!-- One announcement per state change, so a screen reader hears
|
|
443
472
|
"Draft saved" once rather than on every keystroke. -->
|
|
@@ -494,7 +523,9 @@ import {
|
|
|
494
523
|
splitDynamicFormData,
|
|
495
524
|
} from '../../utils/dynamicProperties.js'
|
|
496
525
|
import { shouldShow } from '../../utils/fieldCondition.js'
|
|
526
|
+
import { resolveNextcloudGroup, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
|
|
497
527
|
import { objectDisplayName } from '../../utils/objectName.js'
|
|
528
|
+
import { languageOptions, resolveDefaultToken, timezoneOptions } from '../../utils/pickerOptions.js'
|
|
498
529
|
import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
499
530
|
import { fieldsFromSchema, isTenantProperty } from '../../utils/schema.js'
|
|
500
531
|
import { resolveNextcloudUser, searchNextcloudUsers } from '../../utils/userAutocomplete.js'
|
|
@@ -1025,6 +1056,13 @@ export default {
|
|
|
1025
1056
|
* `dynamicToken` guards the definitions fetch.
|
|
1026
1057
|
*/
|
|
1027
1058
|
prefillToken: '',
|
|
1059
|
+
/**
|
|
1060
|
+
* The inline create in progress, or null: `{ field, schema,
|
|
1061
|
+
* register, slug, initialData, resolve }`. Set when "Create" is
|
|
1062
|
+
* chosen on a select-or-create reference; renders a nested form
|
|
1063
|
+
* for the referenced schema.
|
|
1064
|
+
*/
|
|
1065
|
+
nestedCreate: null,
|
|
1028
1066
|
}
|
|
1029
1067
|
},
|
|
1030
1068
|
|
|
@@ -1295,6 +1333,7 @@ export default {
|
|
|
1295
1333
|
.filter((field) => shouldShow(field, this.formData))
|
|
1296
1334
|
.map((field) => this.applySemanticResolution(field))
|
|
1297
1335
|
.map((field) => this.degradeUnresolvableReference(field))
|
|
1336
|
+
.map((field) => this.applyCodePicker(field))
|
|
1298
1337
|
},
|
|
1299
1338
|
|
|
1300
1339
|
/**
|
|
@@ -1809,11 +1848,16 @@ export default {
|
|
|
1809
1848
|
// Create mode: initialize with field defaults
|
|
1810
1849
|
const data = {}
|
|
1811
1850
|
for (const field of this.resolvedFields) {
|
|
1851
|
+
const tokenDefault = field.defaultToken ? resolveDefaultToken(field.defaultToken, field) : null
|
|
1812
1852
|
if (field.default !== null && field.default !== undefined) {
|
|
1813
1853
|
data[field.key] = field.default
|
|
1854
|
+
} else if (tokenDefault !== null) {
|
|
1855
|
+
// `x-default: current-language` / `current-timezone`:
|
|
1856
|
+
// a NEW object starts with the user's own value.
|
|
1857
|
+
data[field.key] = tokenDefault
|
|
1814
1858
|
} else if (field.widget === 'checkbox') {
|
|
1815
1859
|
data[field.key] = false
|
|
1816
|
-
} else if (field.widget === 'tags' || field.widget === 'multiselect' || field.widget === 'user-multiselect') {
|
|
1860
|
+
} else if (field.widget === 'tags' || field.widget === 'multiselect' || field.widget === 'user-multiselect' || field.widget === 'group-multiselect') {
|
|
1817
1861
|
data[field.key] = []
|
|
1818
1862
|
} else if (field.widget === 'code') {
|
|
1819
1863
|
data[field.key] = ''
|
|
@@ -1880,10 +1924,35 @@ export default {
|
|
|
1880
1924
|
this.resolveUserLabel(uid)
|
|
1881
1925
|
}
|
|
1882
1926
|
}
|
|
1927
|
+
} else if (this.isGroupField(field) || this.isGroupArrayField(field)) {
|
|
1928
|
+
const value = this.formData[field.key]
|
|
1929
|
+
for (const gid of (Array.isArray(value) ? value : [value])) {
|
|
1930
|
+
if (gid) {
|
|
1931
|
+
this.resolveGroupLabel(gid)
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1883
1934
|
}
|
|
1884
1935
|
}
|
|
1885
1936
|
},
|
|
1886
1937
|
|
|
1938
|
+
/**
|
|
1939
|
+
* Resolve a stored group id to its display name and cache it in
|
|
1940
|
+
* `referenceLabels`. No-op once cached; falls back to the gid.
|
|
1941
|
+
*
|
|
1942
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
1943
|
+
* @param {string} gid The stored group id.
|
|
1944
|
+
* @return {Promise<void>}
|
|
1945
|
+
*/
|
|
1946
|
+
async resolveGroupLabel(gid) {
|
|
1947
|
+
if (!gid || this.referenceLabels[gid]) {
|
|
1948
|
+
return
|
|
1949
|
+
}
|
|
1950
|
+
const option = await resolveNextcloudGroup(gid)
|
|
1951
|
+
if (option && option.id) {
|
|
1952
|
+
this.referenceLabels = { ...this.referenceLabels, [option.id]: option.label || String(option.id) }
|
|
1953
|
+
}
|
|
1954
|
+
},
|
|
1955
|
+
|
|
1887
1956
|
/**
|
|
1888
1957
|
* Resolve a single user UID to its display name (edit mode) and cache it
|
|
1889
1958
|
* in `referenceLabels` so the select shows the name instead of the UID.
|
|
@@ -2380,6 +2449,67 @@ export default {
|
|
|
2380
2449
|
return !!(field && field.userPicker && field.userPicker.multiple)
|
|
2381
2450
|
},
|
|
2382
2451
|
|
|
2452
|
+
/**
|
|
2453
|
+
* Whether a field is a single Nextcloud group (`format: 'nc-group'` or
|
|
2454
|
+
* `referenceType: 'nextcloud-group'`). Stores the group id.
|
|
2455
|
+
*
|
|
2456
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
2457
|
+
* @param {object} field The field definition
|
|
2458
|
+
* @return {boolean}
|
|
2459
|
+
*/
|
|
2460
|
+
isGroupField(field) {
|
|
2461
|
+
return !!(field && field.groupPicker && !field.groupPicker.multiple)
|
|
2462
|
+
},
|
|
2463
|
+
|
|
2464
|
+
/**
|
|
2465
|
+
* Whether a field is an array of Nextcloud groups.
|
|
2466
|
+
*
|
|
2467
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
2468
|
+
* @param {object} field The field definition
|
|
2469
|
+
* @return {boolean}
|
|
2470
|
+
*/
|
|
2471
|
+
isGroupArrayField(field) {
|
|
2472
|
+
return !!(field && field.groupPicker && field.groupPicker.multiple)
|
|
2473
|
+
},
|
|
2474
|
+
|
|
2475
|
+
/**
|
|
2476
|
+
* Whether a single-value field stores an id that maps to a label
|
|
2477
|
+
* fetched elsewhere: an object reference, a user or a group.
|
|
2478
|
+
*
|
|
2479
|
+
* @param {object} field The field definition
|
|
2480
|
+
* @return {boolean}
|
|
2481
|
+
*/
|
|
2482
|
+
isIdPickerField(field) {
|
|
2483
|
+
return this.isReferenceField(field) || this.isUserField(field) || this.isGroupField(field)
|
|
2484
|
+
},
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* The multi-value sibling of `isIdPickerField`.
|
|
2488
|
+
*
|
|
2489
|
+
* @param {object} field The field definition
|
|
2490
|
+
* @return {boolean}
|
|
2491
|
+
*/
|
|
2492
|
+
isIdPickerArrayField(field) {
|
|
2493
|
+
return this.isReferenceArrayField(field) || this.isUserArrayField(field) || this.isGroupArrayField(field)
|
|
2494
|
+
},
|
|
2495
|
+
|
|
2496
|
+
/**
|
|
2497
|
+
* Turn a language or time zone field into a searchable select over
|
|
2498
|
+
* a fixed code list: labels in the user's language, the code stored.
|
|
2499
|
+
* Other fields pass through unchanged.
|
|
2500
|
+
*
|
|
2501
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
2502
|
+
* @param {object} field A resolved field descriptor.
|
|
2503
|
+
* @return {object} The field, as a `select` when it is a code picker.
|
|
2504
|
+
*/
|
|
2505
|
+
applyCodePicker(field) {
|
|
2506
|
+
if (!field || (field.widget !== 'language' && field.widget !== 'timezone')) {
|
|
2507
|
+
return field
|
|
2508
|
+
}
|
|
2509
|
+
const list = field.widget === 'language' ? languageOptions() : timezoneOptions()
|
|
2510
|
+
return { ...field, widget: 'select', codePicker: field.widget, enum: list.codes, enumLabels: list.labels }
|
|
2511
|
+
},
|
|
2512
|
+
|
|
2383
2513
|
/**
|
|
2384
2514
|
* Check if a field has an async enum (function instead of static array).
|
|
2385
2515
|
* Object references (`$ref`) and Nextcloud user references are treated
|
|
@@ -2389,7 +2519,7 @@ export default {
|
|
|
2389
2519
|
* @return {boolean}
|
|
2390
2520
|
*/
|
|
2391
2521
|
isAsyncEnum(field) {
|
|
2392
|
-
return typeof field.enum === 'function' || this.
|
|
2522
|
+
return typeof field.enum === 'function' || this.isIdPickerField(field)
|
|
2393
2523
|
},
|
|
2394
2524
|
|
|
2395
2525
|
/**
|
|
@@ -2401,7 +2531,7 @@ export default {
|
|
|
2401
2531
|
* @return {boolean}
|
|
2402
2532
|
*/
|
|
2403
2533
|
isAsyncItemsEnum(field) {
|
|
2404
|
-
return !!(field.items && typeof field.items.enum === 'function') || this.
|
|
2534
|
+
return !!(field.items && typeof field.items.enum === 'function') || this.isIdPickerArrayField(field)
|
|
2405
2535
|
},
|
|
2406
2536
|
|
|
2407
2537
|
/**
|
|
@@ -2468,12 +2598,119 @@ export default {
|
|
|
2468
2598
|
* @return {void}
|
|
2469
2599
|
*/
|
|
2470
2600
|
onReferenceSelected(field, value) {
|
|
2601
|
+
if (Array.isArray(value)) {
|
|
2602
|
+
this.updateField(field.key, value)
|
|
2603
|
+
return
|
|
2604
|
+
}
|
|
2471
2605
|
this.updateField(field.key, value || null)
|
|
2472
2606
|
if (value) {
|
|
2473
2607
|
this.applyTemplateFill(field, value)
|
|
2474
2608
|
}
|
|
2475
2609
|
},
|
|
2476
2610
|
|
|
2611
|
+
/**
|
|
2612
|
+
* The value CnResourceSelect binds for a select-or-create reference:
|
|
2613
|
+
* an array of ids for a multi-value reference, else the id string.
|
|
2614
|
+
*
|
|
2615
|
+
* @param {object} field The reference field descriptor.
|
|
2616
|
+
* @return {string|string[]} The bound value.
|
|
2617
|
+
*/
|
|
2618
|
+
referenceModelValue(field) {
|
|
2619
|
+
const value = this.formData[field.key]
|
|
2620
|
+
if (field.reference && field.reference.multiple) {
|
|
2621
|
+
return Array.isArray(value) ? value.map((v) => String(v)) : []
|
|
2622
|
+
}
|
|
2623
|
+
return value !== null && value !== undefined ? String(value) : ''
|
|
2624
|
+
},
|
|
2625
|
+
|
|
2626
|
+
/**
|
|
2627
|
+
* "Create" on a select-or-create reference: open the referenced
|
|
2628
|
+
* schema's own form, prefilled with the typed term, on top of this one.
|
|
2629
|
+
*
|
|
2630
|
+
* Resolves with the created object once that form is saved, or with
|
|
2631
|
+
* null when it is closed without saving. CnResourceSelect then selects
|
|
2632
|
+
* the new object. When the referenced schema cannot be loaded, the
|
|
2633
|
+
* term alone is saved as before, so the field never dead-ends.
|
|
2634
|
+
*
|
|
2635
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
2636
|
+
* @param {object} field The reference field descriptor.
|
|
2637
|
+
* @param {string} term The typed search term.
|
|
2638
|
+
* @return {Promise<object|null>} The created object, or null.
|
|
2639
|
+
*/
|
|
2640
|
+
async openNestedCreate(field, term) {
|
|
2641
|
+
const store = this.getObjectStore()
|
|
2642
|
+
const register = this.referenceRegister(field)
|
|
2643
|
+
if (!store || !register || !field.reference) {
|
|
2644
|
+
return null
|
|
2645
|
+
}
|
|
2646
|
+
const schemaRef = String(field.reference.schema)
|
|
2647
|
+
const slug = store.createObjectTypeSlug(register, schemaRef)
|
|
2648
|
+
if (!store.objectTypeRegistry[slug]) {
|
|
2649
|
+
store.registerObjectType(slug, schemaRef, register)
|
|
2650
|
+
}
|
|
2651
|
+
const labelField = this.referenceLabelField(field)
|
|
2652
|
+
let schema = null
|
|
2653
|
+
try {
|
|
2654
|
+
schema = await store.fetchSchema(slug)
|
|
2655
|
+
} catch {
|
|
2656
|
+
schema = null
|
|
2657
|
+
}
|
|
2658
|
+
if (!schema || !schema.properties) {
|
|
2659
|
+
return store.saveObject(slug, { [labelField]: term })
|
|
2660
|
+
}
|
|
2661
|
+
const initialData = {}
|
|
2662
|
+
if (term && schema.properties[labelField]) {
|
|
2663
|
+
initialData[labelField] = term
|
|
2664
|
+
}
|
|
2665
|
+
return new Promise((resolve) => {
|
|
2666
|
+
this.nestedCreate = { field, schema, register, slug, initialData, resolve }
|
|
2667
|
+
})
|
|
2668
|
+
},
|
|
2669
|
+
|
|
2670
|
+
/**
|
|
2671
|
+
* The nested create form was confirmed: save the object, hand it to
|
|
2672
|
+
* the waiting picker and close the nested form. A refused save keeps
|
|
2673
|
+
* the nested form open with the server's message.
|
|
2674
|
+
*
|
|
2675
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
2676
|
+
* @param {object} formData The nested form's values.
|
|
2677
|
+
* @return {Promise<void>}
|
|
2678
|
+
*/
|
|
2679
|
+
async onNestedCreateConfirm(formData) {
|
|
2680
|
+
const pending = this.nestedCreate
|
|
2681
|
+
if (!pending) {
|
|
2682
|
+
return
|
|
2683
|
+
}
|
|
2684
|
+
const store = this.getObjectStore()
|
|
2685
|
+
const created = store ? await store.saveObject(pending.slug, formData) : null
|
|
2686
|
+
if (!created) {
|
|
2687
|
+
const error = store && store.errors ? store.errors[pending.slug] : null
|
|
2688
|
+
const message = (error && error.message)
|
|
2689
|
+
|| t('nextcloud-vue', 'The item could not be created.')
|
|
2690
|
+
const dialog = this.$refs.nestedCreateDialog
|
|
2691
|
+
if (dialog && typeof dialog.setValidationErrors === 'function') {
|
|
2692
|
+
dialog.setValidationErrors({}, message)
|
|
2693
|
+
}
|
|
2694
|
+
return
|
|
2695
|
+
}
|
|
2696
|
+
this.nestedCreate = null
|
|
2697
|
+
pending.resolve(created)
|
|
2698
|
+
},
|
|
2699
|
+
|
|
2700
|
+
/**
|
|
2701
|
+
* The nested create form was closed without saving: leave the field
|
|
2702
|
+
* as it was.
|
|
2703
|
+
*
|
|
2704
|
+
* @return {void}
|
|
2705
|
+
*/
|
|
2706
|
+
onNestedCreateClose() {
|
|
2707
|
+
const pending = this.nestedCreate
|
|
2708
|
+
this.nestedCreate = null
|
|
2709
|
+
if (pending) {
|
|
2710
|
+
pending.resolve(null)
|
|
2711
|
+
}
|
|
2712
|
+
},
|
|
2713
|
+
|
|
2477
2714
|
/**
|
|
2478
2715
|
* Copy template values off a selected/created reference object into the
|
|
2479
2716
|
* form, per the field's `fillFrom` map (`{ formKey: sourceKey }`). When
|
|
@@ -2920,9 +3157,14 @@ export default {
|
|
|
2920
3157
|
}
|
|
2921
3158
|
this.asyncState = newState
|
|
2922
3159
|
|
|
2923
|
-
// Trigger initial load for each async field
|
|
3160
|
+
// Trigger initial load for each async field. A select-or-create
|
|
3161
|
+
// reference loads its own options (CnResourceSelect), so it is
|
|
3162
|
+
// skipped here rather than fetched twice.
|
|
2924
3163
|
this.$nextTick(() => {
|
|
2925
3164
|
for (const field of fields) {
|
|
3165
|
+
if (field.allowCreate && field.reference) {
|
|
3166
|
+
continue
|
|
3167
|
+
}
|
|
2926
3168
|
if (this.isAsyncEnum(field) || this.isAsyncItemsEnum(field)) {
|
|
2927
3169
|
this.loadAsyncOptions(field, '')
|
|
2928
3170
|
}
|
|
@@ -2963,6 +3205,18 @@ export default {
|
|
|
2963
3205
|
if (Object.keys(labels).length > 0) {
|
|
2964
3206
|
this.referenceLabels = { ...this.referenceLabels, ...labels }
|
|
2965
3207
|
}
|
|
3208
|
+
} else if (this.isGroupField(field) || this.isGroupArrayField(field)) {
|
|
3209
|
+
// Nextcloud group: search groups, store the gid, show the name.
|
|
3210
|
+
results = await searchNextcloudGroups(query)
|
|
3211
|
+
const labels = {}
|
|
3212
|
+
for (const opt of results) {
|
|
3213
|
+
if (opt && opt.id) {
|
|
3214
|
+
labels[opt.id] = opt.label || String(opt.id)
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
if (Object.keys(labels).length > 0) {
|
|
3218
|
+
this.referenceLabels = { ...this.referenceLabels, ...labels }
|
|
3219
|
+
}
|
|
2966
3220
|
} else {
|
|
2967
3221
|
const enumFn = typeof field.enum === 'function' ? field.enum : field.items.enum
|
|
2968
3222
|
results = await enumFn(query)
|
|
@@ -3021,7 +3275,7 @@ export default {
|
|
|
3021
3275
|
* @return {object|null}
|
|
3022
3276
|
*/
|
|
3023
3277
|
getEffectiveSelectedOption(field) {
|
|
3024
|
-
if (this.
|
|
3278
|
+
if (this.isIdPickerField(field)) {
|
|
3025
3279
|
// Reference / user fields store the UUID/UID — resolve it to a
|
|
3026
3280
|
// display option `{ id, label }` (label from the resolved-labels
|
|
3027
3281
|
// cache, falling back to the id until it loads).
|
|
@@ -3048,7 +3302,7 @@ export default {
|
|
|
3048
3302
|
* @param {object|null} option The selected option
|
|
3049
3303
|
*/
|
|
3050
3304
|
onEffectiveSelectChange(field, option) {
|
|
3051
|
-
if (this.
|
|
3305
|
+
if (this.isIdPickerField(field)) {
|
|
3052
3306
|
// Reference / user fields store the chosen id (UUID / UID),
|
|
3053
3307
|
// not the full option. Cache its label so the selection displays.
|
|
3054
3308
|
if (option && option.id) {
|
|
@@ -3057,7 +3311,7 @@ export default {
|
|
|
3057
3311
|
this.updateField(field.key, option ? option.id : null)
|
|
3058
3312
|
// Reference options are label-only ({id,label}) — pass the id so
|
|
3059
3313
|
// template pre-fill fetches the full object.
|
|
3060
|
-
if (option && option.id) {
|
|
3314
|
+
if (option && option.id && this.isReferenceField(field)) {
|
|
3061
3315
|
this.applyTemplateFill(field, String(option.id))
|
|
3062
3316
|
}
|
|
3063
3317
|
} else if (this.isAsyncEnum(field)) {
|
|
@@ -3089,7 +3343,7 @@ export default {
|
|
|
3089
3343
|
* @return {Array}
|
|
3090
3344
|
*/
|
|
3091
3345
|
getEffectiveSelectedArrayOptions(field) {
|
|
3092
|
-
if (this.
|
|
3346
|
+
if (this.isIdPickerArrayField(field)) {
|
|
3093
3347
|
// Reference / user arrays store an array of ids (UUIDs / UIDs) —
|
|
3094
3348
|
// resolve each to a display option `{ id, label }`.
|
|
3095
3349
|
const uuids = this.formData[field.key]
|
|
@@ -3112,7 +3366,7 @@ export default {
|
|
|
3112
3366
|
* @param {Array} options The selected options
|
|
3113
3367
|
*/
|
|
3114
3368
|
onEffectiveMultiSelectChange(field, options) {
|
|
3115
|
-
if (this.
|
|
3369
|
+
if (this.isIdPickerArrayField(field)) {
|
|
3116
3370
|
// Reference / user arrays store an array of ids (UUIDs / UIDs).
|
|
3117
3371
|
// Cache labels so the chips still display the human names.
|
|
3118
3372
|
const list = options || []
|
|
@@ -3408,6 +3662,11 @@ export default {
|
|
|
3408
3662
|
margin-inline-end: auto;
|
|
3409
3663
|
}
|
|
3410
3664
|
|
|
3665
|
+
.cn-form-dialog__switch-wrapper {
|
|
3666
|
+
display: flex;
|
|
3667
|
+
flex-direction: column;
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3411
3670
|
.cn-form-dialog__form {
|
|
3412
3671
|
display: flex;
|
|
3413
3672
|
flex-direction: column;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
-->
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
|
-
<div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain }" :style="wrapperStyle">
|
|
7
|
+
<div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain, 'cn-header-widget--with-views': viewOptions.length > 0 }" :style="wrapperStyle">
|
|
8
8
|
<div
|
|
9
9
|
v-if="hasOverlay"
|
|
10
10
|
class="cn-header-widget__overlay"
|
|
@@ -34,6 +34,16 @@
|
|
|
34
34
|
{{ ctaLabel }}
|
|
35
35
|
</a>
|
|
36
36
|
</div>
|
|
37
|
+
<!-- A view switch at the right of the heading (`content.views`): "My
|
|
38
|
+
work / My team" as a segmented control whose options are routes. -->
|
|
39
|
+
<div v-if="viewOptions.length > 0" class="cn-header-widget__views">
|
|
40
|
+
<CnSegmentedControl
|
|
41
|
+
:options="viewOptions"
|
|
42
|
+
:modelValue="activeView"
|
|
43
|
+
:ariaLabel="viewsLabel"
|
|
44
|
+
data-testid="cn-header-widget-views"
|
|
45
|
+
@update:modelValue="onViewChange" />
|
|
46
|
+
</div>
|
|
37
47
|
<img
|
|
38
48
|
v-if="hasBackgroundImage"
|
|
39
49
|
class="cn-header-widget__probe"
|
|
@@ -47,6 +57,7 @@
|
|
|
47
57
|
<script>
|
|
48
58
|
import { getCurrentUser } from '@nextcloud/auth'
|
|
49
59
|
import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
|
|
60
|
+
import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
|
|
50
61
|
import { resolveImageUrl } from '../../utils/resolveImageUrl.js'
|
|
51
62
|
|
|
52
63
|
const ALLOWED_OVERLAY_MODES = ['none', 'tint', 'gradient-bottom']
|
|
@@ -99,6 +110,8 @@ const VERTICAL_ALIGN_FLEX = Object.freeze({
|
|
|
99
110
|
export default {
|
|
100
111
|
name: 'CnHeaderWidget',
|
|
101
112
|
|
|
113
|
+
components: { CnSegmentedControl },
|
|
114
|
+
|
|
102
115
|
inject: {
|
|
103
116
|
/**
|
|
104
117
|
* Host translate function provided by CnAppRoot as
|
|
@@ -115,9 +128,12 @@ export default {
|
|
|
115
128
|
* Persisted widget content: `{title, subtitle, backgroundImageUrl,
|
|
116
129
|
* backgroundImageFileId, backgroundColor, overlayMode, overlayColor,
|
|
117
130
|
* overlayOpacity, textColor, textAlign, verticalAlign, height, cta,
|
|
118
|
-
* greeting, showDate, plain}`. All fields are optional except
|
|
119
|
-
* (or `greeting`); unknown enum values collapse to documented
|
|
120
|
-
* and the renderer never throws.
|
|
131
|
+
* greeting, showDate, plain, views}`. All fields are optional except
|
|
132
|
+
* `title` (or `greeting`); unknown enum values collapse to documented
|
|
133
|
+
* defaults and the renderer never throws. `views` is
|
|
134
|
+
* `{ ariaLabel?, options: [{ label, route, params? }] }`: a segmented
|
|
135
|
+
* control at the right of the heading whose checked option is the
|
|
136
|
+
* current route; choosing another pushes its route.
|
|
121
137
|
*
|
|
122
138
|
* @type {object}
|
|
123
139
|
*/
|
|
@@ -198,6 +214,55 @@ export default {
|
|
|
198
214
|
return this.now instanceof Date ? this.now : new Date()
|
|
199
215
|
},
|
|
200
216
|
|
|
217
|
+
/**
|
|
218
|
+
* The view switch's options from `content.views.options`: each
|
|
219
|
+
* `{ label, route, params? }` with a non-empty label and route becomes
|
|
220
|
+
* a segmented-control option keyed on its route name (the label goes
|
|
221
|
+
* through the host translate function). Empty without a router, so
|
|
222
|
+
* the switch cannot offer routes it cannot follow.
|
|
223
|
+
*
|
|
224
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
|
|
225
|
+
* @return {Array<{value: string, label: string}>}
|
|
226
|
+
*/
|
|
227
|
+
viewOptions() {
|
|
228
|
+
const raw = this.content && this.content.views && this.content.views.options
|
|
229
|
+
if (!Array.isArray(raw) || !this.$router) {
|
|
230
|
+
return []
|
|
231
|
+
}
|
|
232
|
+
return raw
|
|
233
|
+
.filter((o) => o && typeof o.label === 'string' && o.label !== '' && typeof o.route === 'string' && o.route !== '')
|
|
234
|
+
.map((o) => ({ value: o.route, label: this.cnTranslate(o.label) }))
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The checked view: the option whose route is the current route, else
|
|
239
|
+
* the first option (a switch always has one checked option).
|
|
240
|
+
*
|
|
241
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
|
|
242
|
+
* @return {string|null}
|
|
243
|
+
*/
|
|
244
|
+
activeView() {
|
|
245
|
+
const options = this.viewOptions
|
|
246
|
+
if (options.length === 0) {
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
const current = this.$route && this.$route.name
|
|
250
|
+
const match = options.find((o) => o.value === current)
|
|
251
|
+
return match ? match.value : options[0].value
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Accessible name of the view switch: `content.views.ariaLabel`
|
|
256
|
+
* through the host translate function, else "View".
|
|
257
|
+
*
|
|
258
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
|
|
259
|
+
* @return {string}
|
|
260
|
+
*/
|
|
261
|
+
viewsLabel() {
|
|
262
|
+
const label = this.content && this.content.views && this.content.views.ariaLabel
|
|
263
|
+
return label ? this.cnTranslate(label) : t('nextcloud-vue', 'View')
|
|
264
|
+
},
|
|
265
|
+
|
|
201
266
|
/**
|
|
202
267
|
* Whether the plain presentation is on (no coloured background).
|
|
203
268
|
*
|
|
@@ -490,7 +555,9 @@ export default {
|
|
|
490
555
|
return {
|
|
491
556
|
position: 'relative',
|
|
492
557
|
'z-index': 1,
|
|
493
|
-
|
|
558
|
+
// Beside a view switch the content shares the row instead of
|
|
559
|
+
// taking the whole width (see .cn-header-widget--with-views).
|
|
560
|
+
width: this.viewOptions.length > 0 ? 'auto' : '100%',
|
|
494
561
|
height: '100%',
|
|
495
562
|
display: 'flex',
|
|
496
563
|
'flex-direction': 'column',
|
|
@@ -604,6 +671,26 @@ export default {
|
|
|
604
671
|
},
|
|
605
672
|
|
|
606
673
|
methods: {
|
|
674
|
+
/**
|
|
675
|
+
* Follow the chosen view: push its route (with the option's `params`
|
|
676
|
+
* when declared). Nothing happens for the route already shown.
|
|
677
|
+
*
|
|
678
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
|
|
679
|
+
* @param {string} route The chosen option's route name.
|
|
680
|
+
* @return {void}
|
|
681
|
+
*/
|
|
682
|
+
onViewChange(route) {
|
|
683
|
+
if (!this.$router || !route || route === (this.$route && this.$route.name)) {
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
const declared = (this.content.views.options || []).find((o) => o && o.route === route) || {}
|
|
687
|
+
const target = { name: route }
|
|
688
|
+
if (declared.params && typeof declared.params === 'object') {
|
|
689
|
+
target.params = declared.params
|
|
690
|
+
}
|
|
691
|
+
this.$router.push(target)
|
|
692
|
+
},
|
|
693
|
+
|
|
607
694
|
/**
|
|
608
695
|
* Mark the background image as broken so the renderer falls back to
|
|
609
696
|
* the solid colour.
|
|
@@ -655,6 +742,29 @@ export default {
|
|
|
655
742
|
border-radius: 0;
|
|
656
743
|
}
|
|
657
744
|
|
|
745
|
+
/* With a view switch the heading and the switch share one row, the switch
|
|
746
|
+
at the right and bottom-aligned with the heading; on a narrow card the
|
|
747
|
+
switch wraps under it. */
|
|
748
|
+
.cn-header-widget--with-views {
|
|
749
|
+
display: flex;
|
|
750
|
+
flex-wrap: wrap;
|
|
751
|
+
align-items: flex-end;
|
|
752
|
+
justify-content: space-between;
|
|
753
|
+
gap: calc(2 * var(--default-grid-baseline)) calc(4 * var(--default-grid-baseline));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.cn-header-widget--with-views .cn-header-widget__content {
|
|
757
|
+
flex: 1 1 320px;
|
|
758
|
+
width: auto;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.cn-header-widget__views {
|
|
762
|
+
position: relative;
|
|
763
|
+
z-index: 1;
|
|
764
|
+
flex: 0 0 auto;
|
|
765
|
+
padding: 16px;
|
|
766
|
+
}
|
|
767
|
+
|
|
658
768
|
.cn-header-widget__date {
|
|
659
769
|
font-size: 14px;
|
|
660
770
|
line-height: 1.4;
|
|
@@ -726,7 +836,7 @@ export default {
|
|
|
726
836
|
height: 1px;
|
|
727
837
|
opacity: 0;
|
|
728
838
|
pointer-events: none;
|
|
729
|
-
|
|
839
|
+
inset-inline-start: -9999px;
|
|
730
840
|
top: -9999px;
|
|
731
841
|
}
|
|
732
842
|
|