@conduction/nextcloud-vue 2.56.0-beta.6 → 2.56.0-beta.7
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/CnActionsMenu/CnActionsMenu.vue2.js +14 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +39 -0
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +46 -4
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +224 -9
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +79 -25
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js +42 -3
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js +39 -6
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +46 -0
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +11 -4
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js +10 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js.map +1 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +39 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +16 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +117 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +147 -89
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +10 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +259 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +131 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +10 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +116 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +24 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +503 -28
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +300 -142
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js +10 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js +237 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js +120 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js +10 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +77 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js +27 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +169 -12
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +15 -7
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js +37 -3
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js +44 -16
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +132 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js +10 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js +150 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js +106 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js +9 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +47 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js +17 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +49 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js +6 -0
- package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js +46 -0
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js +15 -3
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +37 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +29 -13
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js +50 -3
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js +10 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +186 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +46 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +11 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +298 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +77 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js +10 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js +169 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js +48 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js.map +1 -0
- package/dist/esm/components/CnTabs/CnTab.vue2.js +35 -0
- package/dist/esm/components/CnTabs/CnTab.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTab.vue3.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue2.js +120 -7
- package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue3.js +57 -5
- package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +59 -4
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +6 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +10 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +484 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +115 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js +10 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js +189 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js +74 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +82 -8
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +54 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +54 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +931 -4
- package/dist/esm/utils/dateVariant.js +124 -0
- package/dist/esm/utils/dateVariant.js.map +1 -0
- package/dist/esm/utils/detailActionModel.js +260 -0
- package/dist/esm/utils/detailActionModel.js.map +1 -0
- package/dist/esm/utils/dueRule.js +84 -0
- package/dist/esm/utils/dueRule.js.map +1 -0
- package/dist/esm/utils/fetchAggregate.js +37 -1
- package/dist/esm/utils/fetchAggregate.js.map +1 -1
- package/dist/esm/utils/fetchFilterCounts.js +186 -0
- package/dist/esm/utils/fetchFilterCounts.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +5 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/readPath.js +30 -0
- package/dist/esm/utils/readPath.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/nextcloud-vue.cjs.js +14970 -8991
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +931 -4
- package/l10n/en.json +54 -1
- package/l10n/nl.json +54 -1
- package/package.json +1 -1
- package/scripts/.jsdoc-baselines.json +3 -0
- package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
- package/src/components/CnAppNav/CnAppNav.md +26 -0
- package/src/components/CnAppNav/CnAppNav.vue +104 -1
- package/src/components/CnBannerWidget/CnBannerWidget.vue +365 -10
- package/src/components/CnBannerWidgetForm/CnBannerWidgetForm.vue +65 -3
- package/src/components/CnBoardView/CnBoardView.vue +77 -0
- package/src/components/CnBrandStripe/CnBrandStripe.md +22 -0
- package/src/components/CnBrandStripe/CnBrandStripe.vue +100 -0
- package/src/components/CnBrandStripe/index.js +4 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +207 -1
- package/src/components/CnConversationThread/CnConversationThread.md +51 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +457 -0
- package/src/components/CnConversationThread/CnConversationWidget.vue +144 -0
- package/src/components/CnConversationThread/index.js +4 -0
- package/src/components/CnDetailPage/CnDetailPage.md +16 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +683 -48
- package/src/components/CnDocumentReviewList/CnDocumentReviewList.md +55 -0
- package/src/components/CnDocumentReviewList/CnDocumentReviewList.vue +384 -0
- package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +108 -0
- package/src/components/CnDocumentReviewList/index.js +4 -0
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +189 -14
- package/src/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue +53 -3
- package/src/components/CnIndexPage/CnIndexPage.vue +135 -3
- package/src/components/CnNextStepCard/CnNextStepCard.md +40 -0
- package/src/components/CnNextStepCard/CnNextStepCard.vue +319 -0
- package/src/components/CnNextStepCard/CnNextStepWidget.vue +59 -0
- package/src/components/CnNextStepCard/index.js +4 -0
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +49 -0
- package/src/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue +6 -0
- package/src/components/CnObjectKanban/CnObjectKanban.vue +86 -2
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +3 -0
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +60 -1
- package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +50 -3
- package/src/components/CnSegmentedControl/CnSegmentedControl.md +46 -0
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +291 -0
- package/src/components/CnSegmentedControl/index.js +4 -0
- package/src/components/CnStackedBarWidget/CnStackedBarWidget.md +15 -0
- package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +414 -0
- package/src/components/CnStackedBarWidget/index.js +14 -0
- package/src/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue +224 -0
- package/src/components/CnStackedBarWidgetForm/index.js +4 -0
- package/src/components/CnTabs/CnTab.vue +35 -0
- package/src/components/CnTabs/CnTabs.vue +244 -10
- package/src/components/CnTabsWidget/CnTabsWidget.vue +63 -4
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.md +31 -0
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +668 -0
- package/src/components/CnWeekStripWidget/index.js +14 -0
- package/src/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue +265 -0
- package/src/components/CnWeekStripWidgetForm/index.js +4 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +74 -0
- package/src/components/index.js +11 -0
- package/src/css/detail-page.css +47 -0
- package/src/index.js +9 -0
- package/src/schemas/app-manifest-v2.schema.json +389 -1
- package/src/schemas/app-manifest.schema.json +2 -1
- package/src/utils/dateVariant.js +120 -0
- package/src/utils/detailActionModel.js +256 -0
- package/src/utils/dueRule.js +94 -0
- package/src/utils/fetchAggregate.js +36 -0
- package/src/utils/fetchFilterCounts.js +182 -0
- package/src/utils/index.js +1 -0
- package/src/utils/libraryWidgetKeys.js +5 -0
- package/src/utils/readPath.js +27 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnObjectDataWidgetForm.vue3.js","sources":["../../../../src/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue?vue&type=template&id=10b5a40f&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-data-form\">\n\t\t<h4 class=\"cn-data-form__section\">\n\t\t\t{{ t('nextcloud-vue', 'Data widget') }}\n\t\t</h4>\n\n\t\t<NcTextField\n\t\t\t:modelValue=\"title\"\n\t\t\t:label=\"t('nextcloud-vue', 'Title')\"\n\t\t\tplaceholder=\"Data\"\n\t\t\t@update:modelValue=\"updateField('title', $event)\" />\n\n\t\t<div class=\"cn-data-form__row2\">\n\t\t\t<NcTextField\n\t\t\t\t:modelValue=\"source.register\"\n\t\t\t\t:label=\"t('nextcloud-vue', 'Register')\"\n\t\t\t\t:placeholder=\"contextSource.register || 'pipelinq'\"\n\t\t\t\t@update:modelValue=\"updateSource('register', $event)\" />\n\t\t\t<NcTextField\n\t\t\t\t:modelValue=\"source.schema\"\n\t\t\t\t:label=\"t('nextcloud-vue', 'Schema')\"\n\t\t\t\t:placeholder=\"contextSource.schema || 'lead'\"\n\t\t\t\t@update:modelValue=\"updateSource('schema', $event)\" />\n\t\t</div>\n\n\t\t<div class=\"cn-data-form__layout\">\n\t\t\t<span class=\"cn-data-form__layout-label\">{{ t('nextcloud-vue', 'Layout') }}</span>\n\t\t\t<div class=\"cn-data-form__presets\" role=\"group\" :aria-label=\"t('nextcloud-vue', 'Column layout')\">\n\t\t\t\t<button\n\t\t\t\t\tv-for=\"preset in layoutPresets\"\n\t\t\t\t\t:key=\"preset.value\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-data-form__preset\"\n\t\t\t\t\t:class=\"{ 'cn-data-form__preset--active': columns === preset.value }\"\n\t\t\t\t\t:aria-pressed=\"columns === preset.value\"\n\t\t\t\t\t@click=\"setColumns(preset.value)\">\n\t\t\t\t\t{{ preset.label }}\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<NcTextField\n\t\t\t\ttype=\"number\"\n\t\t\t\t:modelValue=\"String(columns)\"\n\t\t\t\t:label=\"t('nextcloud-vue', 'Columns')\"\n\t\t\t\tclass=\"cn-data-form__columns\"\n\t\t\t\t@update:modelValue=\"updateField('columns', Number($event))\" />\n\t\t</div>\n\n\t\t<h4 class=\"cn-data-form__section\">\n\t\t\t{{ t('nextcloud-vue', 'Properties') }}\n\t\t</h4>\n\n\t\t<p v-if=\"!rows.length\" class=\"cn-data-form__hint\">\n\t\t\t{{ t('nextcloud-vue', 'Enter a register and schema to configure properties.') }}\n\t\t</p>\n\t\t<p v-else class=\"cn-data-form__hint\">\n\t\t\t{{ t('nextcloud-vue', 'Drag a property by its handle to reorder. The order applies to both the widget and the edit form.') }}\n\t\t</p>\n\n\t\t<div\n\t\t\tv-for=\"(row, index) in rows\"\n\t\t\t:key=\"row.key\"\n\t\t\tclass=\"cn-data-form__prop\"\n\t\t\t:class=\"{ 'cn-data-form__prop--hidden': row.hidden, 'cn-data-form__prop--dragging': dragIndex === index }\"\n\t\t\tdraggable=\"true\"\n\t\t\t@dragstart=\"onDragStart(index)\"\n\t\t\t@dragover.prevent\n\t\t\t@drop=\"onDrop(index)\"\n\t\t\t@dragend=\"dragIndex = null\">\n\t\t\t<div class=\"cn-data-form__prop-head\">\n\t\t\t\t<span class=\"cn-data-form__grip\" :title=\"t('nextcloud-vue', 'Drag to reorder')\" aria-hidden=\"true\">⋮⋮</span>\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\t:modelValue=\"!row.hidden\"\n\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\t@update:modelValue=\"setRow(row.key, 'hidden', !$event)\">\n\t\t\t\t\t<span class=\"cn-data-form__prop-name\">{{ row.key }}</span>\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"!row.hidden\" class=\"cn-data-form__prop-body\">\n\t\t\t\t<NcTextField\n\t\t\t\t\t:modelValue=\"row.label\"\n\t\t\t\t\t:label=\"t('nextcloud-vue', 'Label')\"\n\t\t\t\t\t:placeholder=\"row.key\"\n\t\t\t\t\t@update:modelValue=\"setRow(row.key, 'label', $event)\" />\n\t\t\t\t<NcTextField\n\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t:modelValue=\"String(row.gridColumn)\"\n\t\t\t\t\t:label=\"t('nextcloud-vue', 'Span')\"\n\t\t\t\t\t@update:modelValue=\"setRow(row.key, 'gridColumn', Number($event))\" />\n\t\t\t\t<NcSelect\n\t\t\t\t\t:modelValue=\"row.widget\"\n\t\t\t\t\t:options=\"widgetOptions\"\n\t\t\t\t\t:inputLabel=\"t('nextcloud-vue', 'Editor')\"\n\t\t\t\t\t:clearable=\"false\"\n\t\t\t\t\t@update:modelValue=\"setRow(row.key, 'widget', $event)\" />\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\t:modelValue=\"row.editable\"\n\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\t@update:modelValue=\"setRow(row.key, 'editable', $event)\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Editable') }}\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcCheckboxRadioSwitch, NcSelect, NcTextField } from '@nextcloud/vue'\nimport { fetchSchemaProperties } from '../../utils/fetchSchemaProperties.js'\n\nconst DEFAULT_CONTENT = Object.freeze({\n\ttitle: '',\n\tregister: '',\n\tschema: '',\n\tcolumns: 3,\n\toverrides: {},\n})\n\nconst WIDGET_OPTIONS = ['auto', 'text', 'textarea', 'number', 'select', 'multiselect', 'tags', 'checkbox', 'date', 'datetime', 'email', 'url']\n\n/**\n * CnObjectDataWidgetForm — the config sub-form for a `data` widget\n * (`CnObjectDataWidget`). Edits the widget title, grid column count, and a\n * per-property override map: visibility, display order, grid span, edit-widget\n * type, label, and editability — discovered from the register/schema. Emits\n * `update:content` with `{ title, register, schema, columns, overrides }`,\n * where `overrides` contains only the non-default entries. Used by\n * `CnAddWidgetModal` + the cog editor; the resolved overrides are forwarded to\n * `CnObjectDataWidget`'s `overrides` prop on detail pages.\n */\nexport default {\n\tname: 'CnObjectDataWidgetForm',\n\n\tcomponents: { NcTextField, NcSelect, NcCheckboxRadioSwitch },\n\n\t/**\n\t * Fallback object context (provided by CnDetailPage as a reactive\n\t * `{ objectId, object, register, schema }` ref). Used to resolve the\n\t * register/schema when the widget being edited is the page's default Data\n\t * widget, which inherits them from the page rather than storing them — so the\n\t * property list still loads even though the form's own Register/Schema fields\n\t * start empty.\n\t */\n\tinject: {\n\t\tcnObjectContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The placement being edited (pre-fills from `editingWidget.content`), or null.\n\t\t *\n\t\t * @type {{content: object}|null}\n\t\t */\n\t\teditingWidget: { type: Object, default: null },\n\t\t/**\n\t\t * Initial content values when not editing (registry defaults).\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tvalue: { type: Object, default: () => ({ ...DEFAULT_CONTENT }) },\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted with the assembled content blob on every field change.\n\t\t *\n\t\t * @event update:content\n\t\t * @type {object}\n\t\t */\n\t\t'update:content',\n\t],\n\n\tdata() {\n\t\tconst initial = this.editingWidget?.content || this.value || {}\n\t\treturn {\n\t\t\ttitle: initial.title ?? '',\n\t\t\tcolumns: Number.isFinite(initial.columns) ? initial.columns : 3,\n\t\t\tsource: { register: initial.register ?? '', schema: initial.schema ?? '' },\n\t\t\toverrides: { ...(initial.overrides || {}) },\n\t\t\trows: [],\n\t\t\t/** Index of the property row currently being dragged, or null. */\n\t\t\tdragIndex: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Edit-widget type options ('auto' = derive from schema). */\n\t\twidgetOptions() {\n\t\t\treturn WIDGET_OPTIONS\n\t\t},\n\n\t\t/** The inherited register/schema (shown as placeholders when not overridden). */\n\t\tcontextSource() {\n\t\t\treturn this.unwrapContext()\n\t\t},\n\n\t\t/** Quick column-layout presets (1 = stacked beneath each other). */\n\t\tlayoutPresets() {\n\t\t\treturn [\n\t\t\t\t{ value: 1, label: t('nextcloud-vue', 'Stacked') },\n\t\t\t\t{ value: 2, label: t('nextcloud-vue', '2 columns') },\n\t\t\t\t{ value: 3, label: t('nextcloud-vue', '3 columns') },\n\t\t\t]\n\t\t},\n\n\t\t/** The assembled content blob from the current field values. */\n\t\tassembledContent() {\n\t\t\treturn {\n\t\t\t\ttitle: this.title,\n\t\t\t\tregister: this.source.register,\n\t\t\t\tschema: this.source.schema,\n\t\t\t\tcolumns: this.columns,\n\t\t\t\toverrides: this.buildOverrides(),\n\t\t\t}\n\t\t},\n\t},\n\n\twatch: {\n\t\t'source.register': 'loadFields',\n\t\t'source.schema': 'loadFields',\n\t},\n\n\tmounted() {\n\t\tthis.loadFields()\n\t},\n\n\tmethods: {\n\t\tt,\n\t\t/**\n\t\t * Unwrap the injected object context (a Vue ref or plain object) to a\n\t\t * plain `{ register, schema }`. Returns empties when no context is\n\t\t * provided (e.g. a dashboard surface).\n\t\t *\n\t\t * @return {{register: string, schema: string}} the resolved context.\n\t\t */\n\t\tunwrapContext() {\n\t\t\tconst c = this.cnObjectContext\n\t\t\tconst v = c && typeof c === 'object' && 'value' in c ? c.value : c\n\t\t\treturn { register: (v && v.register) || '', schema: (v && v.schema) || '' }\n\t\t},\n\n\t\t/**\n\t\t * Resolve the schema's property names and build editable rows, merging\n\t\t * any persisted overrides onto each.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFields() {\n\t\t\tconst ctx = this.unwrapContext()\n\t\t\tconst register = this.source.register || ctx.register\n\t\t\tconst schema = this.source.schema || ctx.schema\n\t\t\t// includeId: the data widget renders the `id` column, so list it here\n\t\t\t// too — the user can then hide it like any other property.\n\t\t\tconst fields = await fetchSchemaProperties(register, schema, { includeId: true })\n\t\t\tthis.rows = fields.map((key) => {\n\t\t\t\tconst o = this.overrides[key] || {}\n\t\t\t\treturn {\n\t\t\t\t\tkey,\n\t\t\t\t\thidden: o.hidden === true,\n\t\t\t\t\tlabel: o.label ?? '',\n\t\t\t\t\torder: typeof o.order === 'number' ? o.order : null,\n\t\t\t\t\tgridColumn: Number.isFinite(o.gridColumn) ? o.gridColumn : 1,\n\t\t\t\t\twidget: o.widget || 'auto',\n\t\t\t\t\teditable: o.editable !== false,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Set a top-level field and emit.\n\t\t *\n\t\t * @param {'title'|'columns'} field The data key to write.\n\t\t * @param {string|number} value The widget title, or the column count.\n\t\t * @return {void}\n\t\t */\n\t\tupdateField(field, value) {\n\t\t\tthis[field] = value\n\t\t\tthis.emitChange()\n\t\t},\n\n\t\t/**\n\t\t * Apply a column-layout preset (1 = stacked) and emit.\n\t\t *\n\t\t * @param {number} value The column count to set.\n\t\t * @return {void}\n\t\t */\n\t\tsetColumns(value) {\n\t\t\tthis.columns = value\n\t\t\tthis.emitChange()\n\t\t},\n\n\t\t/**\n\t\t * Remember which row a drag started on.\n\t\t *\n\t\t * @param {number} index The row index being dragged.\n\t\t * @return {void}\n\t\t */\n\t\tonDragStart(index) {\n\t\t\tthis.dragIndex = index\n\t\t},\n\n\t\t/**\n\t\t * Reorder the property rows on drop: move the dragged row to the drop\n\t\t * target, then stamp every row with a sequential `order` so the new order\n\t\t * persists into the overrides map (applied to both the widget and the edit\n\t\t * form). No-op when dropping a row onto itself.\n\t\t *\n\t\t * @param {number} index The drop-target row index.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(index) {\n\t\t\tconst from = this.dragIndex\n\t\t\tthis.dragIndex = null\n\t\t\tif (from === null || from === index) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst moved = this.rows.splice(from, 1)[0]\n\t\t\tthis.rows.splice(index, 0, moved)\n\t\t\tthis.rows.forEach((row, i) => {\n\t\t\t\trow.order = i\n\t\t\t})\n\t\t\tthis.emitChange()\n\t\t},\n\n\t\t/**\n\t\t * Set a source sub-field and emit. Leaving either empty falls back to the\n\t\t * injected object context.\n\t\t *\n\t\t * @param {'register'|'schema'} field The `source` sub-key to write.\n\t\t * @param {string} value The chosen register or schema slug.\n\t\t * @return {void}\n\t\t */\n\t\tupdateSource(field, value) {\n\t\t\tthis.source[field] = value\n\t\t\tthis.emitChange()\n\t\t},\n\n\t\t/**\n\t\t * Mutate one property row and re-emit.\n\t\t *\n\t\t * @param {string} key The property name.\n\t\t * @param {string} field The row field to set.\n\t\t * @param {unknown} value The new value.\n\t\t * @return {void}\n\t\t */\n\t\tsetRow(key, field, value) {\n\t\t\tconst row = this.rows.find((r) => r.key === key)\n\t\t\tif (!row) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\trow[field] = value\n\t\t\tthis.emitChange()\n\t\t},\n\n\t\t/**\n\t\t * Collapse the rows into a minimal overrides map — only non-default\n\t\t * fields are persisted so the stored config stays small.\n\t\t *\n\t\t * @return {object} The overrides map keyed by property name.\n\t\t */\n\t\tbuildOverrides() {\n\t\t\tconst out = {}\n\t\t\tfor (const row of this.rows) {\n\t\t\t\tconst o = {}\n\t\t\t\tif (row.hidden) {\n\t\t\t\t\to.hidden = true\n\t\t\t\t}\n\t\t\t\tif (row.label && row.label.trim() !== '') {\n\t\t\t\t\to.label = row.label\n\t\t\t\t}\n\t\t\t\tif (typeof row.order === 'number') {\n\t\t\t\t\to.order = row.order\n\t\t\t\t}\n\t\t\t\tif (Number.isFinite(row.gridColumn) && row.gridColumn !== 1) {\n\t\t\t\t\to.gridColumn = row.gridColumn\n\t\t\t\t}\n\t\t\t\tif (row.widget && row.widget !== 'auto') {\n\t\t\t\t\to.widget = row.widget\n\t\t\t\t}\n\t\t\t\tif (row.editable === false) {\n\t\t\t\t\to.editable = false\n\t\t\t\t}\n\t\t\t\tif (Object.keys(o).length) {\n\t\t\t\t\tout[row.key] = o\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Emit the assembled content. */\n\t\temitChange() {\n\t\t\tthis.$emit('update:content', this.assembledContent)\n\t\t},\n\n\t\t/**\n\t\t * Validate the form; an empty array means valid.\n\t\t *\n\t\t * @return {string[]} the validation errors.\n\t\t */\n\t\tvalidate() {\n\t\t\tconst errors = []\n\t\t\t// register/schema may be inherited from the page (injected\n\t\t\t// cnObjectContext) on a detail page — the widget renders against the\n\t\t\t// page's object regardless. Only require them when neither the form\n\t\t\t// fields NOR the context supply them (e.g. a dashboard data widget).\n\t\t\tconst ctx = this.unwrapContext()\n\t\t\tconst register = this.source.register || ctx.register\n\t\t\tconst schema = this.source.schema || ctx.schema\n\t\t\tif (!register || !schema) {\n\t\t\t\terrors.push(t('nextcloud-vue', 'A register and schema are required'))\n\t\t\t}\n\t\t\treturn errors\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-data-form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-data-form__section {\n\tmargin: 8px 0 0;\n\tfont-size: 0.8em;\n\ttext-transform: uppercase;\n\tletter-spacing: 0.03em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-data-form__hint {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-data-form__row2 {\n\tdisplay: flex;\n\tgap: 12px;\n\talign-items: flex-end;\n}\n\n.cn-data-form__row2 > * {\n\tflex: 1;\n\tmin-width: 0;\n}\n\n.cn-data-form__layout {\n\tdisplay: flex;\n\talign-items: flex-end;\n\tgap: 12px;\n\tflex-wrap: wrap;\n}\n\n.cn-data-form__layout-label {\n\talign-self: center;\n\tfont-weight: 600;\n}\n\n.cn-data-form__presets {\n\tdisplay: inline-flex;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\toverflow: hidden;\n}\n\n.cn-data-form__preset {\n\tborder: none;\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tpadding: 6px 12px;\n\tcursor: pointer;\n\tborder-inline-start: 1px solid var(--color-border);\n}\n\n.cn-data-form__preset:first-child {\n\tborder-inline-start: none;\n}\n\n.cn-data-form__preset--active {\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text);\n\tfont-weight: 600;\n}\n\n.cn-data-form__columns {\n\twidth: 90px;\n\tflex: 0 0 auto;\n}\n\n.cn-data-form__prop {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\tpadding: 8px 10px;\n}\n\n.cn-data-form__prop--hidden {\n\topacity: 0.6;\n}\n\n.cn-data-form__prop--dragging {\n\topacity: 0.4;\n\tborder-style: dashed;\n}\n\n.cn-data-form__prop-head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n}\n\n.cn-data-form__grip {\n\tcursor: grab;\n\tcolor: var(--color-text-maxcontrast);\n\tletter-spacing: -2px;\n\tuser-select: none;\n}\n\n.cn-data-form__prop-name {\n\tfont-weight: 600;\n}\n\n.cn-data-form__prop-body {\n\tdisplay: grid;\n\tgrid-template-columns: 2fr 1fr 1fr 1.5fr;\n\tgap: 10px;\n\talign-items: center;\n\tmargin-top: 8px;\n}\n</style>\n"],"names":["_createElementVNode","_toDisplayString","_createVNode","_createElementBlock","_Fragment","_renderList","_openBlock","_normalizeClass"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnObjectDataWidgetForm.vue3.js","sources":["../../../../src/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue?vue&type=template&id=10b5a40f&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-data-form\">\n\t\t<h4 class=\"cn-data-form__section\">\n\t\t\t{{ t('nextcloud-vue', 'Data widget') }}\n\t\t</h4>\n\n\t\t<NcTextField\n\t\t\t:modelValue=\"title\"\n\t\t\t:label=\"t('nextcloud-vue', 'Title')\"\n\t\t\tplaceholder=\"Data\"\n\t\t\t@update:modelValue=\"updateField('title', $event)\" />\n\n\t\t<div class=\"cn-data-form__row2\">\n\t\t\t<NcTextField\n\t\t\t\t:modelValue=\"source.register\"\n\t\t\t\t:label=\"t('nextcloud-vue', 'Register')\"\n\t\t\t\t:placeholder=\"contextSource.register || 'pipelinq'\"\n\t\t\t\t@update:modelValue=\"updateSource('register', $event)\" />\n\t\t\t<NcTextField\n\t\t\t\t:modelValue=\"source.schema\"\n\t\t\t\t:label=\"t('nextcloud-vue', 'Schema')\"\n\t\t\t\t:placeholder=\"contextSource.schema || 'lead'\"\n\t\t\t\t@update:modelValue=\"updateSource('schema', $event)\" />\n\t\t</div>\n\n\t\t<div class=\"cn-data-form__layout\">\n\t\t\t<span class=\"cn-data-form__layout-label\">{{ t('nextcloud-vue', 'Layout') }}</span>\n\t\t\t<div class=\"cn-data-form__presets\" role=\"group\" :aria-label=\"t('nextcloud-vue', 'Column layout')\">\n\t\t\t\t<button\n\t\t\t\t\tv-for=\"preset in layoutPresets\"\n\t\t\t\t\t:key=\"preset.value\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-data-form__preset\"\n\t\t\t\t\t:class=\"{ 'cn-data-form__preset--active': columns === preset.value }\"\n\t\t\t\t\t:aria-pressed=\"columns === preset.value\"\n\t\t\t\t\t@click=\"setColumns(preset.value)\">\n\t\t\t\t\t{{ preset.label }}\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<NcTextField\n\t\t\t\ttype=\"number\"\n\t\t\t\t:modelValue=\"String(columns)\"\n\t\t\t\t:label=\"t('nextcloud-vue', 'Columns')\"\n\t\t\t\tclass=\"cn-data-form__columns\"\n\t\t\t\t@update:modelValue=\"updateField('columns', Number($event))\" />\n\t\t</div>\n\n\t\t<h4 class=\"cn-data-form__section\">\n\t\t\t{{ t('nextcloud-vue', 'Properties') }}\n\t\t</h4>\n\n\t\t<p v-if=\"!rows.length\" class=\"cn-data-form__hint\">\n\t\t\t{{ t('nextcloud-vue', 'Enter a register and schema to configure properties.') }}\n\t\t</p>\n\t\t<p v-else class=\"cn-data-form__hint\">\n\t\t\t{{ t('nextcloud-vue', 'Drag a property by its handle to reorder. The order applies to both the widget and the edit form.') }}\n\t\t</p>\n\n\t\t<div\n\t\t\tv-for=\"(row, index) in rows\"\n\t\t\t:key=\"row.key\"\n\t\t\tclass=\"cn-data-form__prop\"\n\t\t\t:class=\"{ 'cn-data-form__prop--hidden': row.hidden, 'cn-data-form__prop--dragging': dragIndex === index }\"\n\t\t\tdraggable=\"true\"\n\t\t\t@dragstart=\"onDragStart(index)\"\n\t\t\t@dragover.prevent\n\t\t\t@drop=\"onDrop(index)\"\n\t\t\t@dragend=\"dragIndex = null\">\n\t\t\t<div class=\"cn-data-form__prop-head\">\n\t\t\t\t<span class=\"cn-data-form__grip\" :title=\"t('nextcloud-vue', 'Drag to reorder')\" aria-hidden=\"true\">⋮⋮</span>\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\t:modelValue=\"!row.hidden\"\n\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\t@update:modelValue=\"setRow(row.key, 'hidden', !$event)\">\n\t\t\t\t\t<span class=\"cn-data-form__prop-name\">{{ row.key }}</span>\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"!row.hidden\" class=\"cn-data-form__prop-body\">\n\t\t\t\t<NcTextField\n\t\t\t\t\t:modelValue=\"row.label\"\n\t\t\t\t\t:label=\"t('nextcloud-vue', 'Label')\"\n\t\t\t\t\t:placeholder=\"row.key\"\n\t\t\t\t\t@update:modelValue=\"setRow(row.key, 'label', $event)\" />\n\t\t\t\t<NcTextField\n\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t:modelValue=\"String(row.gridColumn)\"\n\t\t\t\t\t:label=\"t('nextcloud-vue', 'Span')\"\n\t\t\t\t\t@update:modelValue=\"setRow(row.key, 'gridColumn', Number($event))\" />\n\t\t\t\t<NcSelect\n\t\t\t\t\t:modelValue=\"row.widget\"\n\t\t\t\t\t:options=\"widgetOptions\"\n\t\t\t\t\t:inputLabel=\"t('nextcloud-vue', 'Editor')\"\n\t\t\t\t\t:clearable=\"false\"\n\t\t\t\t\t@update:modelValue=\"setRow(row.key, 'widget', $event)\" />\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\t:modelValue=\"row.editable\"\n\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\t@update:modelValue=\"setRow(row.key, 'editable', $event)\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Editable') }}\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcCheckboxRadioSwitch, NcSelect, NcTextField } from '@nextcloud/vue'\nimport { fetchSchemaProperties } from '../../utils/fetchSchemaProperties.js'\n\nconst DEFAULT_CONTENT = Object.freeze({\n\ttitle: '',\n\tregister: '',\n\tschema: '',\n\tcolumns: 3,\n\toverrides: {},\n})\n\nconst WIDGET_OPTIONS = ['auto', 'text', 'textarea', 'number', 'select', 'multiselect', 'tags', 'checkbox', 'date', 'datetime', 'email', 'url']\n\n/**\n * CnObjectDataWidgetForm — the config sub-form for a `data` widget\n * (`CnObjectDataWidget`). Edits the widget title, grid column count, and a\n * per-property override map: visibility, display order, grid span, edit-widget\n * type, label, and editability — discovered from the register/schema. Emits\n * `update:content` with `{ title, register, schema, columns, overrides }`,\n * where `overrides` contains only the non-default entries. Used by\n * `CnAddWidgetModal` + the cog editor; the resolved overrides are forwarded to\n * `CnObjectDataWidget`'s `overrides` prop on detail pages.\n */\nexport default {\n\tname: 'CnObjectDataWidgetForm',\n\n\tcomponents: { NcTextField, NcSelect, NcCheckboxRadioSwitch },\n\n\t/**\n\t * Fallback object context (provided by CnDetailPage as a reactive\n\t * `{ objectId, object, register, schema }` ref). Used to resolve the\n\t * register/schema when the widget being edited is the page's default Data\n\t * widget, which inherits them from the page rather than storing them — so the\n\t * property list still loads even though the form's own Register/Schema fields\n\t * start empty.\n\t */\n\tinject: {\n\t\tcnObjectContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The placement being edited (pre-fills from `editingWidget.content`), or null.\n\t\t *\n\t\t * @type {{content: object}|null}\n\t\t */\n\t\teditingWidget: { type: Object, default: null },\n\t\t/**\n\t\t * Initial content values when not editing (registry defaults).\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tvalue: { type: Object, default: () => ({ ...DEFAULT_CONTENT }) },\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted with the assembled content blob on every field change.\n\t\t *\n\t\t * @event update:content\n\t\t * @type {object}\n\t\t */\n\t\t'update:content',\n\t],\n\n\tdata() {\n\t\tconst initial = this.editingWidget?.content || this.value || {}\n\t\treturn {\n\t\t\ttitle: initial.title ?? '',\n\t\t\tcolumns: Number.isFinite(initial.columns) ? initial.columns : 3,\n\t\t\tsource: { register: initial.register ?? '', schema: initial.schema ?? '' },\n\t\t\toverrides: { ...(initial.overrides || {}) },\n\t\t\trows: [],\n\t\t\t/** Index of the property row currently being dragged, or null. */\n\t\t\tdragIndex: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Edit-widget type options ('auto' = derive from schema). */\n\t\twidgetOptions() {\n\t\t\treturn WIDGET_OPTIONS\n\t\t},\n\n\t\t/** The inherited register/schema (shown as placeholders when not overridden). */\n\t\tcontextSource() {\n\t\t\treturn this.unwrapContext()\n\t\t},\n\n\t\t/** Quick column-layout presets (1 = stacked beneath each other). */\n\t\tlayoutPresets() {\n\t\t\treturn [\n\t\t\t\t{ value: 1, label: t('nextcloud-vue', 'Stacked') },\n\t\t\t\t{ value: 2, label: t('nextcloud-vue', '2 columns') },\n\t\t\t\t{ value: 3, label: t('nextcloud-vue', '3 columns') },\n\t\t\t]\n\t\t},\n\n\t\t/** The assembled content blob from the current field values. */\n\t\tassembledContent() {\n\t\t\treturn {\n\t\t\t\ttitle: this.title,\n\t\t\t\tregister: this.source.register,\n\t\t\t\tschema: this.source.schema,\n\t\t\t\tcolumns: this.columns,\n\t\t\t\toverrides: this.buildOverrides(),\n\t\t\t}\n\t\t},\n\t},\n\n\twatch: {\n\t\t'source.register': 'loadFields',\n\t\t'source.schema': 'loadFields',\n\t},\n\n\tmounted() {\n\t\tthis.loadFields()\n\t},\n\n\tmethods: {\n\t\tt,\n\t\t/**\n\t\t * Unwrap the injected object context (a Vue ref or plain object) to a\n\t\t * plain `{ register, schema }`. Returns empties when no context is\n\t\t * provided (e.g. a dashboard surface).\n\t\t *\n\t\t * @return {{register: string, schema: string}} the resolved context.\n\t\t */\n\t\tunwrapContext() {\n\t\t\tconst c = this.cnObjectContext\n\t\t\tconst v = c && typeof c === 'object' && 'value' in c ? c.value : c\n\t\t\treturn { register: (v && v.register) || '', schema: (v && v.schema) || '' }\n\t\t},\n\n\t\t/**\n\t\t * Resolve the schema's property names and build editable rows, merging\n\t\t * any persisted overrides onto each.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFields() {\n\t\t\tconst ctx = this.unwrapContext()\n\t\t\tconst register = this.source.register || ctx.register\n\t\t\tconst schema = this.source.schema || ctx.schema\n\t\t\t// includeId: the data widget renders the `id` column, so list it here\n\t\t\t// too — the user can then hide it like any other property.\n\t\t\tconst fields = await fetchSchemaProperties(register, schema, { includeId: true })\n\t\t\tthis.rows = fields.map((key) => {\n\t\t\t\tconst o = this.overrides[key] || {}\n\t\t\t\treturn {\n\t\t\t\t\tkey,\n\t\t\t\t\thidden: o.hidden === true,\n\t\t\t\t\tlabel: o.label ?? '',\n\t\t\t\t\torder: typeof o.order === 'number' ? o.order : null,\n\t\t\t\t\tgridColumn: Number.isFinite(o.gridColumn) ? o.gridColumn : 1,\n\t\t\t\t\twidget: o.widget || 'auto',\n\t\t\t\t\teditable: o.editable !== false,\n\t\t\t\t\t// Not edited here, but carried through, so saving the\n\t\t\t\t\t// cog editor does not drop a manifest-authored formatter.\n\t\t\t\t\tformatter: typeof o.formatter === 'string' ? o.formatter : '',\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Set a top-level field and emit.\n\t\t *\n\t\t * @param {'title'|'columns'} field The data key to write.\n\t\t * @param {string|number} value The widget title, or the column count.\n\t\t * @return {void}\n\t\t */\n\t\tupdateField(field, value) {\n\t\t\tthis[field] = value\n\t\t\tthis.emitChange()\n\t\t},\n\n\t\t/**\n\t\t * Apply a column-layout preset (1 = stacked) and emit.\n\t\t *\n\t\t * @param {number} value The column count to set.\n\t\t * @return {void}\n\t\t */\n\t\tsetColumns(value) {\n\t\t\tthis.columns = value\n\t\t\tthis.emitChange()\n\t\t},\n\n\t\t/**\n\t\t * Remember which row a drag started on.\n\t\t *\n\t\t * @param {number} index The row index being dragged.\n\t\t * @return {void}\n\t\t */\n\t\tonDragStart(index) {\n\t\t\tthis.dragIndex = index\n\t\t},\n\n\t\t/**\n\t\t * Reorder the property rows on drop: move the dragged row to the drop\n\t\t * target, then stamp every row with a sequential `order` so the new order\n\t\t * persists into the overrides map (applied to both the widget and the edit\n\t\t * form). No-op when dropping a row onto itself.\n\t\t *\n\t\t * @param {number} index The drop-target row index.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(index) {\n\t\t\tconst from = this.dragIndex\n\t\t\tthis.dragIndex = null\n\t\t\tif (from === null || from === index) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst moved = this.rows.splice(from, 1)[0]\n\t\t\tthis.rows.splice(index, 0, moved)\n\t\t\tthis.rows.forEach((row, i) => {\n\t\t\t\trow.order = i\n\t\t\t})\n\t\t\tthis.emitChange()\n\t\t},\n\n\t\t/**\n\t\t * Set a source sub-field and emit. Leaving either empty falls back to the\n\t\t * injected object context.\n\t\t *\n\t\t * @param {'register'|'schema'} field The `source` sub-key to write.\n\t\t * @param {string} value The chosen register or schema slug.\n\t\t * @return {void}\n\t\t */\n\t\tupdateSource(field, value) {\n\t\t\tthis.source[field] = value\n\t\t\tthis.emitChange()\n\t\t},\n\n\t\t/**\n\t\t * Mutate one property row and re-emit.\n\t\t *\n\t\t * @param {string} key The property name.\n\t\t * @param {string} field The row field to set.\n\t\t * @param {unknown} value The new value.\n\t\t * @return {void}\n\t\t */\n\t\tsetRow(key, field, value) {\n\t\t\tconst row = this.rows.find((r) => r.key === key)\n\t\t\tif (!row) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\trow[field] = value\n\t\t\tthis.emitChange()\n\t\t},\n\n\t\t/**\n\t\t * Collapse the rows into a minimal overrides map — only non-default\n\t\t * fields are persisted so the stored config stays small.\n\t\t *\n\t\t * @return {object} The overrides map keyed by property name.\n\t\t */\n\t\tbuildOverrides() {\n\t\t\tconst out = {}\n\t\t\tfor (const row of this.rows) {\n\t\t\t\tconst o = {}\n\t\t\t\tif (row.hidden) {\n\t\t\t\t\to.hidden = true\n\t\t\t\t}\n\t\t\t\tif (row.label && row.label.trim() !== '') {\n\t\t\t\t\to.label = row.label\n\t\t\t\t}\n\t\t\t\tif (typeof row.order === 'number') {\n\t\t\t\t\to.order = row.order\n\t\t\t\t}\n\t\t\t\tif (Number.isFinite(row.gridColumn) && row.gridColumn !== 1) {\n\t\t\t\t\to.gridColumn = row.gridColumn\n\t\t\t\t}\n\t\t\t\tif (row.widget && row.widget !== 'auto') {\n\t\t\t\t\to.widget = row.widget\n\t\t\t\t}\n\t\t\t\tif (row.editable === false) {\n\t\t\t\t\to.editable = false\n\t\t\t\t}\n\t\t\t\tif (row.formatter) {\n\t\t\t\t\to.formatter = row.formatter\n\t\t\t\t}\n\t\t\t\tif (Object.keys(o).length) {\n\t\t\t\t\tout[row.key] = o\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Emit the assembled content. */\n\t\temitChange() {\n\t\t\tthis.$emit('update:content', this.assembledContent)\n\t\t},\n\n\t\t/**\n\t\t * Validate the form; an empty array means valid.\n\t\t *\n\t\t * @return {string[]} the validation errors.\n\t\t */\n\t\tvalidate() {\n\t\t\tconst errors = []\n\t\t\t// register/schema may be inherited from the page (injected\n\t\t\t// cnObjectContext) on a detail page — the widget renders against the\n\t\t\t// page's object regardless. Only require them when neither the form\n\t\t\t// fields NOR the context supply them (e.g. a dashboard data widget).\n\t\t\tconst ctx = this.unwrapContext()\n\t\t\tconst register = this.source.register || ctx.register\n\t\t\tconst schema = this.source.schema || ctx.schema\n\t\t\tif (!register || !schema) {\n\t\t\t\terrors.push(t('nextcloud-vue', 'A register and schema are required'))\n\t\t\t}\n\t\t\treturn errors\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-data-form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-data-form__section {\n\tmargin: 8px 0 0;\n\tfont-size: 0.8em;\n\ttext-transform: uppercase;\n\tletter-spacing: 0.03em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-data-form__hint {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-data-form__row2 {\n\tdisplay: flex;\n\tgap: 12px;\n\talign-items: flex-end;\n}\n\n.cn-data-form__row2 > * {\n\tflex: 1;\n\tmin-width: 0;\n}\n\n.cn-data-form__layout {\n\tdisplay: flex;\n\talign-items: flex-end;\n\tgap: 12px;\n\tflex-wrap: wrap;\n}\n\n.cn-data-form__layout-label {\n\talign-self: center;\n\tfont-weight: 600;\n}\n\n.cn-data-form__presets {\n\tdisplay: inline-flex;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\toverflow: hidden;\n}\n\n.cn-data-form__preset {\n\tborder: none;\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tpadding: 6px 12px;\n\tcursor: pointer;\n\tborder-inline-start: 1px solid var(--color-border);\n}\n\n.cn-data-form__preset:first-child {\n\tborder-inline-start: none;\n}\n\n.cn-data-form__preset--active {\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text);\n\tfont-weight: 600;\n}\n\n.cn-data-form__columns {\n\twidth: 90px;\n\tflex: 0 0 auto;\n}\n\n.cn-data-form__prop {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\tpadding: 8px 10px;\n}\n\n.cn-data-form__prop--hidden {\n\topacity: 0.6;\n}\n\n.cn-data-form__prop--dragging {\n\topacity: 0.4;\n\tborder-style: dashed;\n}\n\n.cn-data-form__prop-head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n}\n\n.cn-data-form__grip {\n\tcursor: grab;\n\tcolor: var(--color-text-maxcontrast);\n\tletter-spacing: -2px;\n\tuser-select: none;\n}\n\n.cn-data-form__prop-name {\n\tfont-weight: 600;\n}\n\n.cn-data-form__prop-body {\n\tdisplay: grid;\n\tgrid-template-columns: 2fr 1fr 1fr 1.5fr;\n\tgap: 10px;\n\talign-items: center;\n\tmargin-top: 8px;\n}\n</style>\n"],"names":["_createElementVNode","_toDisplayString","_createVNode","_createElementBlock","_Fragment","_renderList","_openBlock","_normalizeClass"],"mappings":"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"}
|
|
@@ -2,6 +2,7 @@ import { translate } from '@nextcloud/l10n';
|
|
|
2
2
|
import { NcButton, NcLoadingIcon, NcEmptyContent } from '@nextcloud/vue';
|
|
3
3
|
import draggable from '../../node_modules/vuedraggable/dist/vuedraggable.umd.js';
|
|
4
4
|
import ViewColumn from 'vue-material-design-icons/ViewColumn.vue';
|
|
5
|
+
import { dueStateForRow } from '../../utils/dueRule.js';
|
|
5
6
|
import __sfc_main from '../CnCellRenderer/CnCellRenderer.vue.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -135,6 +136,22 @@ var script = {
|
|
|
135
136
|
type: String,
|
|
136
137
|
default: 'id',
|
|
137
138
|
},
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Marks late cards: `{ field, soonDays? }`. `field` is the dot-path to
|
|
142
|
+
* a card's due date. A date before today gets an error edge and the
|
|
143
|
+
* label "Overdue"; a date within `soonDays` (default 3) gets "Due
|
|
144
|
+
* soon" in the warning colour. `null` (the default) marks nothing.
|
|
145
|
+
*
|
|
146
|
+
* The same rule shape the table's date cell uses, so a list and its
|
|
147
|
+
* board agree on what late means.
|
|
148
|
+
*
|
|
149
|
+
* @type {{field: string, soonDays?: number}|null}
|
|
150
|
+
*/
|
|
151
|
+
dueRule: {
|
|
152
|
+
type: Object,
|
|
153
|
+
default: null,
|
|
154
|
+
},
|
|
138
155
|
},
|
|
139
156
|
|
|
140
157
|
emits: ['card-click', 'load-more', 'move', 'move-rejected'],
|
|
@@ -150,6 +167,8 @@ var script = {
|
|
|
150
167
|
// objectId -> { fromValue, toValue, card, originIndex }
|
|
151
168
|
pendingMoves: {},
|
|
152
169
|
dragOriginValue: null,
|
|
170
|
+
// The day late marking measures from.
|
|
171
|
+
today: new Date(),
|
|
153
172
|
}
|
|
154
173
|
},
|
|
155
174
|
|
|
@@ -204,6 +223,33 @@ var script = {
|
|
|
204
223
|
methods: {
|
|
205
224
|
t: translate,
|
|
206
225
|
|
|
226
|
+
/**
|
|
227
|
+
* A card's due state under `dueRule`.
|
|
228
|
+
*
|
|
229
|
+
* @param {object} card The card's row.
|
|
230
|
+
* @return {'overdue'|'soon'|'ok'|null} The state, or null when no rule applies.
|
|
231
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-late-marking-on-board-cards
|
|
232
|
+
*/
|
|
233
|
+
dueState(card) {
|
|
234
|
+
return dueStateForRow(card, this.dueRule, this.today)
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The words a late card carries. Colour alone would not reach a reader
|
|
239
|
+
* who cannot see it, or cannot tell red from amber.
|
|
240
|
+
*
|
|
241
|
+
* @param {object} card The card's row.
|
|
242
|
+
* @return {string} "Overdue", "Due soon", or '' for a card that is fine.
|
|
243
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-late-marking-on-board-cards
|
|
244
|
+
*/
|
|
245
|
+
dueLabel(card) {
|
|
246
|
+
const state = this.dueState(card);
|
|
247
|
+
if (state === 'overdue') {
|
|
248
|
+
return translate('nextcloud-vue', 'Overdue')
|
|
249
|
+
}
|
|
250
|
+
return state === 'soon' ? translate('nextcloud-vue', 'Due soon') : ''
|
|
251
|
+
},
|
|
252
|
+
|
|
207
253
|
/**
|
|
208
254
|
* Rebuild `localColumns` from either the `columns` prop (backend-paginated
|
|
209
255
|
* mode) or `objects` + `groupByField` (locally-derived mode).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnObjectKanban.vue2.js","sources":["../../../../src/components/CnObjectKanban/CnObjectKanban.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-object-kanban\">\n\t\t<div v-if=\"loading\" class=\"cn-object-kanban__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<div v-else-if=\"localColumns.length === 0\" class=\"cn-object-kanban__empty\">\n\t\t\t<!-- @slot empty Custom empty state shown when there are no columns to render. -->\n\t\t\t<slot name=\"empty\">\n\t\t\t\t<NcEmptyContent :name=\"emptyText\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<ViewColumn :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<div v-else class=\"cn-object-kanban__board\">\n\t\t\t<div\n\t\t\t\tv-for=\"column in localColumns\"\n\t\t\t\t:key=\"columnKey(column.value)\"\n\t\t\t\tclass=\"cn-object-kanban__column\">\n\t\t\t\t<div class=\"cn-object-kanban__column-header\">\n\t\t\t\t\t<!-- @slot column-header Override a column's header. -->\n\t\t\t\t\t<!-- @binding {object} column The column ({ value, cards, total, limit, offset }). -->\n\t\t\t\t\t<slot name=\"column-header\" :column=\"column\">\n\t\t\t\t\t\t<span class=\"cn-object-kanban__column-title\">{{ columnLabel(column) }}</span>\n\t\t\t\t\t\t<span class=\"cn-object-kanban__column-count\">{{ column.total }}</span>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- `#item` + `item-key`: vuedraggable@4 (the Vue 3 build)\n\t\t\t\t renders the cards itself from this slot. A `v-for` in the\n\t\t\t\t default slot throws \"draggable element must have an item\n\t\t\t\t slot\" the moment the column paints. -->\n\t\t\t\t<Draggable\n\t\t\t\t\t:list=\"column.cards\"\n\t\t\t\t\ttag=\"div\"\n\t\t\t\t\tclass=\"cn-object-kanban__column-cards\"\n\t\t\t\t\t:itemKey=\"cardKey\"\n\t\t\t\t\tgroup=\"cn-object-kanban-cards\"\n\t\t\t\t\t:data-column-value=\"columnKey(column.value)\"\n\t\t\t\t\t@start=\"onDragStart(column)\"\n\t\t\t\t\t@change=\"onColumnChange($event, column)\">\n\t\t\t\t\t<template #item=\"{ element: card }\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass=\"cn-object-kanban__card\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-object-kanban__card--pending': isPending(card) }\">\n\t\t\t\t\t\t\t<!-- @slot card Fully replace the default card rendering. -->\n\t\t\t\t\t\t\t<!-- @binding {object} object The card's object. -->\n\t\t\t\t\t\t\t<!-- @binding {object} column The column the card currently sits in. -->\n\t\t\t\t\t\t\t<slot name=\"card\" :object=\"card\" :column=\"column\">\n\t\t\t\t\t\t\t\t<div class=\"cn-object-kanban__card-title\" @click=\"onCardClick(card)\">\n\t\t\t\t\t\t\t\t\t{{ cardTitle(card) }}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div v-if=\"cardFields.length\" class=\"cn-object-kanban__card-fields\">\n\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\tv-for=\"field in cardFields\"\n\t\t\t\t\t\t\t\t\t\t:key=\"field\"\n\t\t\t\t\t\t\t\t\t\tclass=\"cn-object-kanban__card-field\">\n\t\t\t\t\t\t\t\t\t\t<CnCellRenderer\n\t\t\t\t\t\t\t\t\t\t\t:value=\"card[field]\"\n\t\t\t\t\t\t\t\t\t\t\t:property=\"schemaProperty(field)\"\n\t\t\t\t\t\t\t\t\t\t\t:truncate=\"40\" />\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</slot>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</template>\n\t\t\t\t</Draggable>\n\n\t\t\t\t<div v-if=\"hasMore(column)\" class=\"cn-object-kanban__load-more\">\n\t\t\t\t\t<NcButton :disabled=\"isColumnLoading(column)\" @click=\"onLoadMore(column)\">\n\t\t\t\t\t\t<template v-if=\"isColumnLoading(column)\" #icon>\n\t\t\t\t\t\t\t<NcLoadingIcon :size=\"16\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ loadMoreLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport draggable from 'vuedraggable'\nimport ViewColumn from 'vue-material-design-icons/ViewColumn.vue'\nimport { CnCellRenderer } from '../CnCellRenderer/index.js'\n\n/**\n * CnObjectKanban — Kanban board over a schema property's distinct values.\n *\n * Renders one column per distinct value of `groupByField` and paginates each\n * column's cards independently (\"load more\", never a whole column at once).\n * Column values/order come from (in precedence order) an explicit\n * `columnOrder`, the `groupByField` schema property's `enum`, or values\n * discovered from `objects` — mirroring OpenRegister's\n * `ViewPresentationService::deriveColumnValues()`.\n *\n * Two rendering modes:\n * - **Pre-built columns** — pass `columns` (the shape returned by\n * `GET /api/views/{id}/kanban`: `[{ value, cards, total, limit, offset }]`);\n * the host owns pagination and re-fetches on `load-more`.\n * - **Flat objects** — pass `objects` + `groupByField` (and optionally\n * `columnOrder`/`schema`); the component derives and paginates columns\n * locally.\n *\n * Dragging a card to another column does NOT write anything itself — there is\n * deliberately no bespoke \"move card\" endpoint (REQ-VIEW-KANBAN-03). The move\n * is applied optimistically to the local board and the component emits\n * `move`; the host performs the actual object PATCH/PUT through the existing\n * guarded write path and reports back:\n * - On success, nothing further is required (the optimistic move already\n * reflects it) — optionally call `resolveMove(id)` to clear the pending\n * visual state sooner.\n * - On a rejected (e.g. illegal lifecycle transition) write, call\n * `rejectMove(id, reason)` — the card snaps back to its origin column and\n * `move-rejected` fires with the reason for the host to surface.\n *\n * ```vue\n * <CnObjectKanban\n * :objects=\"objects\"\n * group-by-field=\"status\"\n * :column-order=\"['todo', 'doing', 'done']\"\n * :card-fields=\"['title', 'assignee']\"\n * :schema=\"schema\"\n * ref=\"kanban\"\n * @move=\"onMove\"\n * @load-more=\"fetchMoreCards\" />\n * ```\n * ```js\n * async onMove({ object, groupByField, fromValue, toValue }) {\n * try {\n * await patchObject(object.id, { [groupByField]: toValue })\n * } catch (e) {\n * this.$refs.kanban.rejectMove(object.id, e.message)\n * }\n * }\n * ```\n */\nexport default {\n\tname: 'CnObjectKanban',\n\n\tcomponents: {\n\t\tDraggable: draggable,\n\t\tNcButton,\n\t\tNcLoadingIcon,\n\t\tNcEmptyContent,\n\t\tViewColumn,\n\t\tCnCellRenderer,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Flat objects to derive columns from (ignored when `columns` is set).\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tobjects: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Pre-built columns, e.g. the response of `GET /api/views/{id}/kanban`.\n\t\t *\n\t\t * @type {Array<{value: string|number, cards: Array<object>, total: number, limit: number, offset: number}>|null}\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The schema property whose distinct values become columns. */\n\t\tgroupByField: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Explicit column order. Takes precedence over the schema's enum order. */\n\t\tcolumnOrder: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Object fields rendered on each card (in order). */\n\t\tcardFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Schema definition, used to resolve enum column order and card field types. */\n\t\tschema: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Overall loading state (initial board fetch). */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Column values currently loading more cards (drives the per-column spinner). */\n\t\tloadingColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Cards shown per column before \"load more\", in local (`objects`-driven) mode. */\n\t\tpageSize: {\n\t\t\ttype: Number,\n\t\t\tdefault: 20,\n\t\t},\n\n\t\t/** Object property used as each card's identity. */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\t},\n\n\temits: ['card-click', 'load-more', 'move', 'move-rejected'],\n\n\tdata() {\n\t\treturn {\n\t\t\tlocalColumns: [],\n\t\t\t// column.value -> full unpaginated card list, local mode only.\n\t\t\tfullCardsByColumn: {},\n\t\t\t// Suppressed while a move is pending so the prop-driven rebuild\n\t\t\t// doesn't clobber the optimistic UI before the host's data catches up.\n\t\t\tsuppressRebuild: false,\n\t\t\t// objectId -> { fromValue, toValue, card, originIndex }\n\t\t\tpendingMoves: {},\n\t\t\tdragOriginValue: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\temptyText() {\n\t\t\treturn t('nextcloud-vue', 'No columns to show')\n\t\t},\n\n\t\tloadMoreLabel() {\n\t\t\treturn t('nextcloud-vue', 'Load more')\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjects: {\n\t\t\thandler() {\n\t\t\t\tif (!this.suppressRebuild) {\n\t\t\t\t\tthis.rebuild()\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\tcolumns: {\n\t\t\thandler() {\n\t\t\t\tif (!this.suppressRebuild) {\n\t\t\t\t\tthis.rebuild()\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\tgroupByField() {\n\t\t\tthis.rebuild()\n\t\t},\n\n\t\tcolumnOrder: {\n\t\t\thandler() {\n\t\t\t\tthis.rebuild()\n\t\t\t},\n\n\t\t\tdeep: true,\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.rebuild()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Rebuild `localColumns` from either the `columns` prop (backend-paginated\n\t\t * mode) or `objects` + `groupByField` (locally-derived mode).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\trebuild() {\n\t\t\tif (Array.isArray(this.columns)) {\n\t\t\t\tthis.localColumns = this.columns.map((column) => ({\n\t\t\t\t\tvalue: column.value,\n\t\t\t\t\tcards: Array.isArray(column.cards) ? [...column.cards] : [],\n\t\t\t\t\ttotal: typeof column.total === 'number' ? column.total : (column.cards || []).length,\n\t\t\t\t\tlimit: column.limit,\n\t\t\t\t\toffset: column.offset,\n\t\t\t\t}))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst values = this.deriveColumnValues()\n\t\t\tconst fullByColumn = {}\n\t\t\tthis.localColumns = values.map((value) => {\n\t\t\t\tconst fullCards = this.objects.filter((object) => object && object[this.groupByField] === value)\n\t\t\t\tfullByColumn[this.columnKey(value)] = fullCards\n\t\t\t\treturn {\n\t\t\t\t\tvalue,\n\t\t\t\t\tcards: fullCards.slice(0, this.pageSize),\n\t\t\t\t\ttotal: fullCards.length,\n\t\t\t\t\tlimit: this.pageSize,\n\t\t\t\t\toffset: 0,\n\t\t\t\t}\n\t\t\t})\n\t\t\tthis.fullCardsByColumn = fullByColumn\n\t\t},\n\n\t\t/**\n\t\t * Derive the ordered column values: `columnOrder` > schema enum order >\n\t\t * distinct values observed in `objects`. Mirrors\n\t\t * `ViewPresentationService::deriveColumnValues()`.\n\t\t *\n\t\t * @return {Array<unknown>}\n\t\t */\n\t\tderiveColumnValues() {\n\t\t\tif (Array.isArray(this.columnOrder) && this.columnOrder.length > 0) {\n\t\t\t\treturn [...this.columnOrder]\n\t\t\t}\n\n\t\t\tconst enumValues = this.schema?.properties?.[this.groupByField]?.enum\n\t\t\tif (Array.isArray(enumValues) && enumValues.length > 0) {\n\t\t\t\treturn [...enumValues]\n\t\t\t}\n\n\t\t\treturn this.discoverDistinctValues()\n\t\t},\n\n\t\t/**\n\t\t * Distinct `groupByField` values observed in `objects`, in first-seen order.\n\t\t *\n\t\t * @return {Array<unknown>}\n\t\t */\n\t\tdiscoverDistinctValues() {\n\t\t\tconst seen = new Set()\n\t\t\tconst values = []\n\t\t\tfor (const object of this.objects) {\n\t\t\t\tconst value = object?.[this.groupByField]\n\t\t\t\tif (value !== undefined && value !== null && !seen.has(value)) {\n\t\t\t\t\tseen.add(value)\n\t\t\t\t\tvalues.push(value)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn values\n\t\t},\n\n\t\t/**\n\t\t * Stable string key for a column value (used as a `:key` and as the\n\t\t * `fullCardsByColumn` index — object/array values are unlikely but guarded).\n\t\t *\n\t\t * @param {unknown} value The column value.\n\t\t * @return {string}\n\t\t */\n\t\tcolumnKey(value) {\n\t\t\treturn typeof value === 'string' || typeof value === 'number' ? String(value) : JSON.stringify(value)\n\t\t},\n\n\t\t/**\n\t\t * The card's identity, per `rowKey`.\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {unknown}\n\t\t */\n\t\tcardKey(card) {\n\t\t\treturn card?.[this.rowKey]\n\t\t},\n\n\t\t/**\n\t\t * Display label for a column: its schema `oneOf[].title` when the\n\t\t * property declares per-value titles, otherwise the raw value.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {string}\n\t\t */\n\t\tcolumnLabel(column) {\n\t\t\tconst oneOf = this.schema?.properties?.[this.groupByField]?.oneOf\n\t\t\tif (Array.isArray(oneOf)) {\n\t\t\t\tconst match = oneOf.find((entry) => entry?.const === column.value)\n\t\t\t\tif (match?.title) {\n\t\t\t\t\treturn match.title\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn String(column.value)\n\t\t},\n\n\t\t/**\n\t\t * Best-effort card title: the schema's configured name field, else\n\t\t * `title`/`name`/the row key.\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {string}\n\t\t */\n\t\tcardTitle(card) {\n\t\t\tconst nameField = this.schema?.configuration?.objectNameField\n\t\t\tif (nameField && card[nameField]) {\n\t\t\t\treturn String(card[nameField])\n\t\t\t}\n\t\t\treturn String(card.title || card.name || card[this.rowKey] || '—')\n\t\t},\n\n\t\t/**\n\t\t * The schema property definition for a card field, if known.\n\t\t *\n\t\t * @param {string} field The field name.\n\t\t * @return {object|undefined}\n\t\t */\n\t\tschemaProperty(field) {\n\t\t\treturn this.schema?.properties?.[field]\n\t\t},\n\n\t\t/**\n\t\t * Whether a column has more cards to load: backend-paginated mode compares\n\t\t * `cards.length` to `total`; local mode compares to the full filtered set.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {boolean}\n\t\t */\n\t\thasMore(column) {\n\t\t\tif (Array.isArray(this.columns)) {\n\t\t\t\treturn column.cards.length < column.total\n\t\t\t}\n\t\t\tconst full = this.fullCardsByColumn[this.columnKey(column.value)] || []\n\t\t\treturn column.cards.length < full.length\n\t\t},\n\n\t\t/**\n\t\t * Whether a column's \"load more\" is in flight.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {boolean}\n\t\t */\n\t\tisColumnLoading(column) {\n\t\t\treturn this.loadingColumns.includes(column.value)\n\t\t},\n\n\t\t/**\n\t\t * Whether a card has an unresolved optimistic move.\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {boolean}\n\t\t */\n\t\tisPending(card) {\n\t\t\treturn Object.hasOwn(this.pendingMoves, this.cardKey(card))\n\t\t},\n\n\t\t/**\n\t\t * Handle a column's \"load more\" click: local mode reveals the next page\n\t\t * from the already-fetched full set; backend-paginated mode only emits —\n\t\t * the host fetches the next page and passes updated `columns`.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {void}\n\t\t */\n\t\tonLoadMore(column) {\n\t\t\tif (!Array.isArray(this.columns)) {\n\t\t\t\tconst full = this.fullCardsByColumn[this.columnKey(column.value)] || []\n\t\t\t\tcolumn.cards = full.slice(0, column.cards.length + this.pageSize)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event load-more Emitted when a column's \"load more\" is clicked.\n\t\t\t * @type {{ value: unknown, offset: number }}\n\t\t\t */\n\t\t\tthis.$emit('load-more', { value: column.value, offset: column.cards.length })\n\t\t},\n\n\t\t/**\n\t\t * Card click (ignored while the card is mid-drag).\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {void}\n\t\t */\n\t\tonCardClick(card) {\n\t\t\t/**\n\t\t\t * @event card-click Emitted when a card is clicked (not dragged).\n\t\t\t * @type {object} The card's object.\n\t\t\t */\n\t\t\tthis.$emit('card-click', card)\n\t\t},\n\n\t\t/**\n\t\t * vuedraggable `@start` — remember which column a drag began in, since\n\t\t * `@change` on the destination column doesn't know the source.\n\t\t *\n\t\t * @param {object} column The column the drag started in.\n\t\t * @return {void}\n\t\t */\n\t\tonDragStart(column) {\n\t\t\tthis.dragOriginValue = column.value\n\t\t},\n\n\t\t/**\n\t\t * vuedraggable `@change` on a column's card list. Only the destination\n\t\t * column's `added` event carries the dropped object; a same-column\n\t\t * reorder (`moved`) or the source column's `removed` are no-ops here.\n\t\t *\n\t\t * @param {object} evt vuedraggable's change event.\n\t\t * @param {object} column The column the event fired on.\n\t\t * @return {void}\n\t\t */\n\t\tonColumnChange(evt, column) {\n\t\t\tif (!evt.added) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst card = evt.added.element\n\t\t\tconst fromValue = this.dragOriginValue\n\t\t\tconst toValue = column.value\n\t\t\tthis.dragOriginValue = null\n\n\t\t\tif (fromValue === toValue) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.commitMove(card, fromValue, toValue, evt.added.newIndex)\n\t\t},\n\n\t\t/**\n\t\t * Record the pending move and emit it for the host to persist. The card\n\t\t * has already been moved optimistically by vuedraggable's v-model splice.\n\t\t *\n\t\t * @param {object} card The moved object.\n\t\t * @param {unknown} fromValue The origin column's value.\n\t\t * @param {unknown} toValue The destination column's value.\n\t\t * @param {number} originIndex The index the card is currently at in the destination column (used only for logging/debugging).\n\t\t * @return {void}\n\t\t */\n\t\tcommitMove(card, fromValue, toValue, originIndex) {\n\t\t\tconst id = this.cardKey(card)\n\t\t\tthis.suppressRebuild = true\n\t\t\tthis.pendingMoves = {\n\t\t\t\t...this.pendingMoves,\n\t\t\t\t[id]: { fromValue, toValue, card, originIndex },\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @event move Emitted after a card is optimistically moved to another\n\t\t\t * column. The host performs the actual object write through the\n\t\t\t * existing guarded PATCH/PUT endpoint and calls `resolveMove`/\n\t\t\t * `rejectMove` on this component to confirm or roll back.\n\t\t\t * @type {{ object: object, groupByField: string, fromValue: unknown, toValue: unknown }}\n\t\t\t */\n\t\t\tthis.$emit('move', {\n\t\t\t\tobject: card,\n\t\t\t\tgroupByField: this.groupByField,\n\t\t\t\tfromValue,\n\t\t\t\ttoValue,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Confirm a pending move succeeded — clears its pending visual state.\n\t\t * The card already sits in the destination column (optimistic UI); no\n\t\t * further mutation is needed.\n\t\t *\n\t\t * @param {unknown} objectId The moved object's `rowKey` value.\n\t\t * @return {void}\n\t\t * @public\n\t\t */\n\t\tresolveMove(objectId) {\n\t\t\tthis.clearPending(objectId)\n\t\t},\n\n\t\t/**\n\t\t * Roll back a rejected move: the card returns to its origin column and\n\t\t * `move-rejected` fires with the server's reason.\n\t\t *\n\t\t * @param {unknown} objectId The moved object's `rowKey` value.\n\t\t * @param {string} [reason] The rejection reason (e.g. an illegal lifecycle\n\t\t * transition message) to surface to the user.\n\t\t * @return {void}\n\t\t * @public\n\t\t */\n\t\trejectMove(objectId, reason) {\n\t\t\tconst pending = this.pendingMoves[objectId]\n\t\t\tif (!pending) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst toColumn = this.localColumns.find((c) => c.value === pending.toValue)\n\t\t\tconst fromColumn = this.localColumns.find((c) => c.value === pending.fromValue)\n\n\t\t\tif (toColumn) {\n\t\t\t\tconst idx = toColumn.cards.findIndex((c) => this.cardKey(c) === objectId)\n\t\t\t\tif (idx !== -1) {\n\t\t\t\t\ttoColumn.cards.splice(idx, 1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (fromColumn) {\n\t\t\t\tfromColumn.cards.push(pending.card)\n\t\t\t}\n\n\t\t\tthis.clearPending(objectId)\n\n\t\t\t/**\n\t\t\t * @event move-rejected Emitted after `rejectMove()` rolls a card back\n\t\t\t * to its origin column.\n\t\t\t * @type {{ object: object, fromValue: unknown, toValue: unknown, reason: (string|undefined) }}\n\t\t\t */\n\t\t\tthis.$emit('move-rejected', {\n\t\t\t\tobject: pending.card,\n\t\t\t\tfromValue: pending.fromValue,\n\t\t\t\ttoValue: pending.toValue,\n\t\t\t\treason,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Remove a move from the pending map and, once none remain, allow\n\t\t * prop-driven rebuilds again.\n\t\t *\n\t\t * @param {unknown} objectId The moved object's `rowKey` value.\n\t\t * @return {void}\n\t\t */\n\t\tclearPending(objectId) {\n\t\t\tconst { [objectId]: _removed, ...rest } = this.pendingMoves\n\t\t\tthis.pendingMoves = rest\n\t\t\tif (Object.keys(rest).length === 0) {\n\t\t\t\tthis.suppressRebuild = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-object-kanban__loading,\n.cn-object-kanban__empty {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tpadding: 40px;\n}\n\n.cn-object-kanban__board {\n\tdisplay: flex;\n\tgap: 16px;\n\toverflow-x: auto;\n\tpadding-bottom: 8px;\n}\n\n.cn-object-kanban__column {\n\tdisplay: flex;\n\tflex-direction: column;\n\tflex: 0 0 280px;\n\tmin-width: 280px;\n\tbackground: var(--color-background-dark);\n\tborder-radius: var(--border-radius-large, 10px);\n\tpadding: 8px;\n}\n\n.cn-object-kanban__column-header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tpadding: 8px;\n\tfont-weight: 600;\n}\n\n.cn-object-kanban__column-count {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: 400;\n}\n\n.cn-object-kanban__column-cards {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-height: 40px;\n}\n\n.cn-object-kanban__card {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius, 8px);\n\tpadding: 10px;\n\tcursor: grab;\n\ttransition: opacity 0.15s ease;\n}\n\n.cn-object-kanban__card--pending {\n\topacity: 0.6;\n}\n\n.cn-object-kanban__card-title {\n\tfont-weight: 500;\n\tcursor: pointer;\n}\n\n.cn-object-kanban__card-fields {\n\tmargin-top: 6px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-object-kanban__load-more {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding-top: 8px;\n}\n</style>\n"],"names":["CnCellRenderer","t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnObjectKanban.vue2.js","sources":["../../../../src/components/CnObjectKanban/CnObjectKanban.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-object-kanban\">\n\t\t<div v-if=\"loading\" class=\"cn-object-kanban__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<div v-else-if=\"localColumns.length === 0\" class=\"cn-object-kanban__empty\">\n\t\t\t<!-- @slot empty Custom empty state shown when there are no columns to render. -->\n\t\t\t<slot name=\"empty\">\n\t\t\t\t<NcEmptyContent :name=\"emptyText\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<ViewColumn :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<div v-else class=\"cn-object-kanban__board\">\n\t\t\t<div\n\t\t\t\tv-for=\"column in localColumns\"\n\t\t\t\t:key=\"columnKey(column.value)\"\n\t\t\t\tclass=\"cn-object-kanban__column\">\n\t\t\t\t<div class=\"cn-object-kanban__column-header\">\n\t\t\t\t\t<!-- @slot column-header Override a column's header. -->\n\t\t\t\t\t<!-- @binding {object} column The column ({ value, cards, total, limit, offset }). -->\n\t\t\t\t\t<slot name=\"column-header\" :column=\"column\">\n\t\t\t\t\t\t<span class=\"cn-object-kanban__column-title\">{{ columnLabel(column) }}</span>\n\t\t\t\t\t\t<span class=\"cn-object-kanban__column-count\">{{ column.total }}</span>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- `#item` + `item-key`: vuedraggable@4 (the Vue 3 build)\n\t\t\t\t renders the cards itself from this slot. A `v-for` in the\n\t\t\t\t default slot throws \"draggable element must have an item\n\t\t\t\t slot\" the moment the column paints. -->\n\t\t\t\t<Draggable\n\t\t\t\t\t:list=\"column.cards\"\n\t\t\t\t\ttag=\"div\"\n\t\t\t\t\tclass=\"cn-object-kanban__column-cards\"\n\t\t\t\t\t:itemKey=\"cardKey\"\n\t\t\t\t\tgroup=\"cn-object-kanban-cards\"\n\t\t\t\t\t:data-column-value=\"columnKey(column.value)\"\n\t\t\t\t\t@start=\"onDragStart(column)\"\n\t\t\t\t\t@change=\"onColumnChange($event, column)\">\n\t\t\t\t\t<template #item=\"{ element: card }\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass=\"cn-object-kanban__card\"\n\t\t\t\t\t\t\t:class=\"{\n\t\t\t\t\t\t\t\t'cn-object-kanban__card--pending': isPending(card),\n\t\t\t\t\t\t\t\t'cn-object-kanban__card--overdue': dueState(card) === 'overdue',\n\t\t\t\t\t\t\t\t'cn-object-kanban__card--soon': dueState(card) === 'soon',\n\t\t\t\t\t\t\t}\">\n\t\t\t\t\t\t\t<!-- @slot card Fully replace the default card rendering. -->\n\t\t\t\t\t\t\t<!-- @binding {object} object The card's object. -->\n\t\t\t\t\t\t\t<!-- @binding {object} column The column the card currently sits in. -->\n\t\t\t\t\t\t\t<!-- @binding {string|null} dueState The card's due state under `dueRule`: `overdue`, `soon`, `ok`, or null without a rule. -->\n\t\t\t\t\t\t\t<slot\n\t\t\t\t\t\t\t\tname=\"card\"\n\t\t\t\t\t\t\t\t:object=\"card\"\n\t\t\t\t\t\t\t\t:column=\"column\"\n\t\t\t\t\t\t\t\t:dueState=\"dueState(card)\">\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"dueLabel(card)\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-object-kanban__due\"\n\t\t\t\t\t\t\t\t\t:class=\"`cn-object-kanban__due--${dueState(card)}`\"\n\t\t\t\t\t\t\t\t\tdata-testid=\"cn-kanban-due\">\n\t\t\t\t\t\t\t\t\t{{ dueLabel(card) }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<div class=\"cn-object-kanban__card-title\" @click=\"onCardClick(card)\">\n\t\t\t\t\t\t\t\t\t{{ cardTitle(card) }}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div v-if=\"cardFields.length\" class=\"cn-object-kanban__card-fields\">\n\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\tv-for=\"field in cardFields\"\n\t\t\t\t\t\t\t\t\t\t:key=\"field\"\n\t\t\t\t\t\t\t\t\t\tclass=\"cn-object-kanban__card-field\">\n\t\t\t\t\t\t\t\t\t\t<CnCellRenderer\n\t\t\t\t\t\t\t\t\t\t\t:value=\"card[field]\"\n\t\t\t\t\t\t\t\t\t\t\t:property=\"schemaProperty(field)\"\n\t\t\t\t\t\t\t\t\t\t\t:truncate=\"40\" />\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</slot>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</template>\n\t\t\t\t</Draggable>\n\n\t\t\t\t<div v-if=\"hasMore(column)\" class=\"cn-object-kanban__load-more\">\n\t\t\t\t\t<NcButton :disabled=\"isColumnLoading(column)\" @click=\"onLoadMore(column)\">\n\t\t\t\t\t\t<template v-if=\"isColumnLoading(column)\" #icon>\n\t\t\t\t\t\t\t<NcLoadingIcon :size=\"16\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ loadMoreLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport draggable from 'vuedraggable'\nimport ViewColumn from 'vue-material-design-icons/ViewColumn.vue'\nimport { dueStateForRow } from '../../utils/dueRule.js'\nimport { CnCellRenderer } from '../CnCellRenderer/index.js'\n\n/**\n * CnObjectKanban — Kanban board over a schema property's distinct values.\n *\n * Renders one column per distinct value of `groupByField` and paginates each\n * column's cards independently (\"load more\", never a whole column at once).\n * Column values/order come from (in precedence order) an explicit\n * `columnOrder`, the `groupByField` schema property's `enum`, or values\n * discovered from `objects` — mirroring OpenRegister's\n * `ViewPresentationService::deriveColumnValues()`.\n *\n * Two rendering modes:\n * - **Pre-built columns** — pass `columns` (the shape returned by\n * `GET /api/views/{id}/kanban`: `[{ value, cards, total, limit, offset }]`);\n * the host owns pagination and re-fetches on `load-more`.\n * - **Flat objects** — pass `objects` + `groupByField` (and optionally\n * `columnOrder`/`schema`); the component derives and paginates columns\n * locally.\n *\n * Dragging a card to another column does NOT write anything itself — there is\n * deliberately no bespoke \"move card\" endpoint (REQ-VIEW-KANBAN-03). The move\n * is applied optimistically to the local board and the component emits\n * `move`; the host performs the actual object PATCH/PUT through the existing\n * guarded write path and reports back:\n * - On success, nothing further is required (the optimistic move already\n * reflects it) — optionally call `resolveMove(id)` to clear the pending\n * visual state sooner.\n * - On a rejected (e.g. illegal lifecycle transition) write, call\n * `rejectMove(id, reason)` — the card snaps back to its origin column and\n * `move-rejected` fires with the reason for the host to surface.\n *\n * ```vue\n * <CnObjectKanban\n * :objects=\"objects\"\n * group-by-field=\"status\"\n * :column-order=\"['todo', 'doing', 'done']\"\n * :card-fields=\"['title', 'assignee']\"\n * :schema=\"schema\"\n * ref=\"kanban\"\n * @move=\"onMove\"\n * @load-more=\"fetchMoreCards\" />\n * ```\n * ```js\n * async onMove({ object, groupByField, fromValue, toValue }) {\n * try {\n * await patchObject(object.id, { [groupByField]: toValue })\n * } catch (e) {\n * this.$refs.kanban.rejectMove(object.id, e.message)\n * }\n * }\n * ```\n */\nexport default {\n\tname: 'CnObjectKanban',\n\n\tcomponents: {\n\t\tDraggable: draggable,\n\t\tNcButton,\n\t\tNcLoadingIcon,\n\t\tNcEmptyContent,\n\t\tViewColumn,\n\t\tCnCellRenderer,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Flat objects to derive columns from (ignored when `columns` is set).\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tobjects: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Pre-built columns, e.g. the response of `GET /api/views/{id}/kanban`.\n\t\t *\n\t\t * @type {Array<{value: string|number, cards: Array<object>, total: number, limit: number, offset: number}>|null}\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The schema property whose distinct values become columns. */\n\t\tgroupByField: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Explicit column order. Takes precedence over the schema's enum order. */\n\t\tcolumnOrder: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Object fields rendered on each card (in order). */\n\t\tcardFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Schema definition, used to resolve enum column order and card field types. */\n\t\tschema: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Overall loading state (initial board fetch). */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Column values currently loading more cards (drives the per-column spinner). */\n\t\tloadingColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Cards shown per column before \"load more\", in local (`objects`-driven) mode. */\n\t\tpageSize: {\n\t\t\ttype: Number,\n\t\t\tdefault: 20,\n\t\t},\n\n\t\t/** Object property used as each card's identity. */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/**\n\t\t * Marks late cards: `{ field, soonDays? }`. `field` is the dot-path to\n\t\t * a card's due date. A date before today gets an error edge and the\n\t\t * label \"Overdue\"; a date within `soonDays` (default 3) gets \"Due\n\t\t * soon\" in the warning colour. `null` (the default) marks nothing.\n\t\t *\n\t\t * The same rule shape the table's date cell uses, so a list and its\n\t\t * board agree on what late means.\n\t\t *\n\t\t * @type {{field: string, soonDays?: number}|null}\n\t\t */\n\t\tdueRule: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['card-click', 'load-more', 'move', 'move-rejected'],\n\n\tdata() {\n\t\treturn {\n\t\t\tlocalColumns: [],\n\t\t\t// column.value -> full unpaginated card list, local mode only.\n\t\t\tfullCardsByColumn: {},\n\t\t\t// Suppressed while a move is pending so the prop-driven rebuild\n\t\t\t// doesn't clobber the optimistic UI before the host's data catches up.\n\t\t\tsuppressRebuild: false,\n\t\t\t// objectId -> { fromValue, toValue, card, originIndex }\n\t\t\tpendingMoves: {},\n\t\t\tdragOriginValue: null,\n\t\t\t// The day late marking measures from.\n\t\t\ttoday: new Date(),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\temptyText() {\n\t\t\treturn t('nextcloud-vue', 'No columns to show')\n\t\t},\n\n\t\tloadMoreLabel() {\n\t\t\treturn t('nextcloud-vue', 'Load more')\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjects: {\n\t\t\thandler() {\n\t\t\t\tif (!this.suppressRebuild) {\n\t\t\t\t\tthis.rebuild()\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\tcolumns: {\n\t\t\thandler() {\n\t\t\t\tif (!this.suppressRebuild) {\n\t\t\t\t\tthis.rebuild()\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\tgroupByField() {\n\t\t\tthis.rebuild()\n\t\t},\n\n\t\tcolumnOrder: {\n\t\t\thandler() {\n\t\t\t\tthis.rebuild()\n\t\t\t},\n\n\t\t\tdeep: true,\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.rebuild()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * A card's due state under `dueRule`.\n\t\t *\n\t\t * @param {object} card The card's row.\n\t\t * @return {'overdue'|'soon'|'ok'|null} The state, or null when no rule applies.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-late-marking-on-board-cards\n\t\t */\n\t\tdueState(card) {\n\t\t\treturn dueStateForRow(card, this.dueRule, this.today)\n\t\t},\n\n\t\t/**\n\t\t * The words a late card carries. Colour alone would not reach a reader\n\t\t * who cannot see it, or cannot tell red from amber.\n\t\t *\n\t\t * @param {object} card The card's row.\n\t\t * @return {string} \"Overdue\", \"Due soon\", or '' for a card that is fine.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-late-marking-on-board-cards\n\t\t */\n\t\tdueLabel(card) {\n\t\t\tconst state = this.dueState(card)\n\t\t\tif (state === 'overdue') {\n\t\t\t\treturn t('nextcloud-vue', 'Overdue')\n\t\t\t}\n\t\t\treturn state === 'soon' ? t('nextcloud-vue', 'Due soon') : ''\n\t\t},\n\n\t\t/**\n\t\t * Rebuild `localColumns` from either the `columns` prop (backend-paginated\n\t\t * mode) or `objects` + `groupByField` (locally-derived mode).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\trebuild() {\n\t\t\tif (Array.isArray(this.columns)) {\n\t\t\t\tthis.localColumns = this.columns.map((column) => ({\n\t\t\t\t\tvalue: column.value,\n\t\t\t\t\tcards: Array.isArray(column.cards) ? [...column.cards] : [],\n\t\t\t\t\ttotal: typeof column.total === 'number' ? column.total : (column.cards || []).length,\n\t\t\t\t\tlimit: column.limit,\n\t\t\t\t\toffset: column.offset,\n\t\t\t\t}))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst values = this.deriveColumnValues()\n\t\t\tconst fullByColumn = {}\n\t\t\tthis.localColumns = values.map((value) => {\n\t\t\t\tconst fullCards = this.objects.filter((object) => object && object[this.groupByField] === value)\n\t\t\t\tfullByColumn[this.columnKey(value)] = fullCards\n\t\t\t\treturn {\n\t\t\t\t\tvalue,\n\t\t\t\t\tcards: fullCards.slice(0, this.pageSize),\n\t\t\t\t\ttotal: fullCards.length,\n\t\t\t\t\tlimit: this.pageSize,\n\t\t\t\t\toffset: 0,\n\t\t\t\t}\n\t\t\t})\n\t\t\tthis.fullCardsByColumn = fullByColumn\n\t\t},\n\n\t\t/**\n\t\t * Derive the ordered column values: `columnOrder` > schema enum order >\n\t\t * distinct values observed in `objects`. Mirrors\n\t\t * `ViewPresentationService::deriveColumnValues()`.\n\t\t *\n\t\t * @return {Array<unknown>}\n\t\t */\n\t\tderiveColumnValues() {\n\t\t\tif (Array.isArray(this.columnOrder) && this.columnOrder.length > 0) {\n\t\t\t\treturn [...this.columnOrder]\n\t\t\t}\n\n\t\t\tconst enumValues = this.schema?.properties?.[this.groupByField]?.enum\n\t\t\tif (Array.isArray(enumValues) && enumValues.length > 0) {\n\t\t\t\treturn [...enumValues]\n\t\t\t}\n\n\t\t\treturn this.discoverDistinctValues()\n\t\t},\n\n\t\t/**\n\t\t * Distinct `groupByField` values observed in `objects`, in first-seen order.\n\t\t *\n\t\t * @return {Array<unknown>}\n\t\t */\n\t\tdiscoverDistinctValues() {\n\t\t\tconst seen = new Set()\n\t\t\tconst values = []\n\t\t\tfor (const object of this.objects) {\n\t\t\t\tconst value = object?.[this.groupByField]\n\t\t\t\tif (value !== undefined && value !== null && !seen.has(value)) {\n\t\t\t\t\tseen.add(value)\n\t\t\t\t\tvalues.push(value)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn values\n\t\t},\n\n\t\t/**\n\t\t * Stable string key for a column value (used as a `:key` and as the\n\t\t * `fullCardsByColumn` index — object/array values are unlikely but guarded).\n\t\t *\n\t\t * @param {unknown} value The column value.\n\t\t * @return {string}\n\t\t */\n\t\tcolumnKey(value) {\n\t\t\treturn typeof value === 'string' || typeof value === 'number' ? String(value) : JSON.stringify(value)\n\t\t},\n\n\t\t/**\n\t\t * The card's identity, per `rowKey`.\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {unknown}\n\t\t */\n\t\tcardKey(card) {\n\t\t\treturn card?.[this.rowKey]\n\t\t},\n\n\t\t/**\n\t\t * Display label for a column: its schema `oneOf[].title` when the\n\t\t * property declares per-value titles, otherwise the raw value.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {string}\n\t\t */\n\t\tcolumnLabel(column) {\n\t\t\tconst oneOf = this.schema?.properties?.[this.groupByField]?.oneOf\n\t\t\tif (Array.isArray(oneOf)) {\n\t\t\t\tconst match = oneOf.find((entry) => entry?.const === column.value)\n\t\t\t\tif (match?.title) {\n\t\t\t\t\treturn match.title\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn String(column.value)\n\t\t},\n\n\t\t/**\n\t\t * Best-effort card title: the schema's configured name field, else\n\t\t * `title`/`name`/the row key.\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {string}\n\t\t */\n\t\tcardTitle(card) {\n\t\t\tconst nameField = this.schema?.configuration?.objectNameField\n\t\t\tif (nameField && card[nameField]) {\n\t\t\t\treturn String(card[nameField])\n\t\t\t}\n\t\t\treturn String(card.title || card.name || card[this.rowKey] || '—')\n\t\t},\n\n\t\t/**\n\t\t * The schema property definition for a card field, if known.\n\t\t *\n\t\t * @param {string} field The field name.\n\t\t * @return {object|undefined}\n\t\t */\n\t\tschemaProperty(field) {\n\t\t\treturn this.schema?.properties?.[field]\n\t\t},\n\n\t\t/**\n\t\t * Whether a column has more cards to load: backend-paginated mode compares\n\t\t * `cards.length` to `total`; local mode compares to the full filtered set.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {boolean}\n\t\t */\n\t\thasMore(column) {\n\t\t\tif (Array.isArray(this.columns)) {\n\t\t\t\treturn column.cards.length < column.total\n\t\t\t}\n\t\t\tconst full = this.fullCardsByColumn[this.columnKey(column.value)] || []\n\t\t\treturn column.cards.length < full.length\n\t\t},\n\n\t\t/**\n\t\t * Whether a column's \"load more\" is in flight.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {boolean}\n\t\t */\n\t\tisColumnLoading(column) {\n\t\t\treturn this.loadingColumns.includes(column.value)\n\t\t},\n\n\t\t/**\n\t\t * Whether a card has an unresolved optimistic move.\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {boolean}\n\t\t */\n\t\tisPending(card) {\n\t\t\treturn Object.hasOwn(this.pendingMoves, this.cardKey(card))\n\t\t},\n\n\t\t/**\n\t\t * Handle a column's \"load more\" click: local mode reveals the next page\n\t\t * from the already-fetched full set; backend-paginated mode only emits —\n\t\t * the host fetches the next page and passes updated `columns`.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {void}\n\t\t */\n\t\tonLoadMore(column) {\n\t\t\tif (!Array.isArray(this.columns)) {\n\t\t\t\tconst full = this.fullCardsByColumn[this.columnKey(column.value)] || []\n\t\t\t\tcolumn.cards = full.slice(0, column.cards.length + this.pageSize)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event load-more Emitted when a column's \"load more\" is clicked.\n\t\t\t * @type {{ value: unknown, offset: number }}\n\t\t\t */\n\t\t\tthis.$emit('load-more', { value: column.value, offset: column.cards.length })\n\t\t},\n\n\t\t/**\n\t\t * Card click (ignored while the card is mid-drag).\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {void}\n\t\t */\n\t\tonCardClick(card) {\n\t\t\t/**\n\t\t\t * @event card-click Emitted when a card is clicked (not dragged).\n\t\t\t * @type {object} The card's object.\n\t\t\t */\n\t\t\tthis.$emit('card-click', card)\n\t\t},\n\n\t\t/**\n\t\t * vuedraggable `@start` — remember which column a drag began in, since\n\t\t * `@change` on the destination column doesn't know the source.\n\t\t *\n\t\t * @param {object} column The column the drag started in.\n\t\t * @return {void}\n\t\t */\n\t\tonDragStart(column) {\n\t\t\tthis.dragOriginValue = column.value\n\t\t},\n\n\t\t/**\n\t\t * vuedraggable `@change` on a column's card list. Only the destination\n\t\t * column's `added` event carries the dropped object; a same-column\n\t\t * reorder (`moved`) or the source column's `removed` are no-ops here.\n\t\t *\n\t\t * @param {object} evt vuedraggable's change event.\n\t\t * @param {object} column The column the event fired on.\n\t\t * @return {void}\n\t\t */\n\t\tonColumnChange(evt, column) {\n\t\t\tif (!evt.added) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst card = evt.added.element\n\t\t\tconst fromValue = this.dragOriginValue\n\t\t\tconst toValue = column.value\n\t\t\tthis.dragOriginValue = null\n\n\t\t\tif (fromValue === toValue) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.commitMove(card, fromValue, toValue, evt.added.newIndex)\n\t\t},\n\n\t\t/**\n\t\t * Record the pending move and emit it for the host to persist. The card\n\t\t * has already been moved optimistically by vuedraggable's v-model splice.\n\t\t *\n\t\t * @param {object} card The moved object.\n\t\t * @param {unknown} fromValue The origin column's value.\n\t\t * @param {unknown} toValue The destination column's value.\n\t\t * @param {number} originIndex The index the card is currently at in the destination column (used only for logging/debugging).\n\t\t * @return {void}\n\t\t */\n\t\tcommitMove(card, fromValue, toValue, originIndex) {\n\t\t\tconst id = this.cardKey(card)\n\t\t\tthis.suppressRebuild = true\n\t\t\tthis.pendingMoves = {\n\t\t\t\t...this.pendingMoves,\n\t\t\t\t[id]: { fromValue, toValue, card, originIndex },\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @event move Emitted after a card is optimistically moved to another\n\t\t\t * column. The host performs the actual object write through the\n\t\t\t * existing guarded PATCH/PUT endpoint and calls `resolveMove`/\n\t\t\t * `rejectMove` on this component to confirm or roll back.\n\t\t\t * @type {{ object: object, groupByField: string, fromValue: unknown, toValue: unknown }}\n\t\t\t */\n\t\t\tthis.$emit('move', {\n\t\t\t\tobject: card,\n\t\t\t\tgroupByField: this.groupByField,\n\t\t\t\tfromValue,\n\t\t\t\ttoValue,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Confirm a pending move succeeded — clears its pending visual state.\n\t\t * The card already sits in the destination column (optimistic UI); no\n\t\t * further mutation is needed.\n\t\t *\n\t\t * @param {unknown} objectId The moved object's `rowKey` value.\n\t\t * @return {void}\n\t\t * @public\n\t\t */\n\t\tresolveMove(objectId) {\n\t\t\tthis.clearPending(objectId)\n\t\t},\n\n\t\t/**\n\t\t * Roll back a rejected move: the card returns to its origin column and\n\t\t * `move-rejected` fires with the server's reason.\n\t\t *\n\t\t * @param {unknown} objectId The moved object's `rowKey` value.\n\t\t * @param {string} [reason] The rejection reason (e.g. an illegal lifecycle\n\t\t * transition message) to surface to the user.\n\t\t * @return {void}\n\t\t * @public\n\t\t */\n\t\trejectMove(objectId, reason) {\n\t\t\tconst pending = this.pendingMoves[objectId]\n\t\t\tif (!pending) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst toColumn = this.localColumns.find((c) => c.value === pending.toValue)\n\t\t\tconst fromColumn = this.localColumns.find((c) => c.value === pending.fromValue)\n\n\t\t\tif (toColumn) {\n\t\t\t\tconst idx = toColumn.cards.findIndex((c) => this.cardKey(c) === objectId)\n\t\t\t\tif (idx !== -1) {\n\t\t\t\t\ttoColumn.cards.splice(idx, 1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (fromColumn) {\n\t\t\t\tfromColumn.cards.push(pending.card)\n\t\t\t}\n\n\t\t\tthis.clearPending(objectId)\n\n\t\t\t/**\n\t\t\t * @event move-rejected Emitted after `rejectMove()` rolls a card back\n\t\t\t * to its origin column.\n\t\t\t * @type {{ object: object, fromValue: unknown, toValue: unknown, reason: (string|undefined) }}\n\t\t\t */\n\t\t\tthis.$emit('move-rejected', {\n\t\t\t\tobject: pending.card,\n\t\t\t\tfromValue: pending.fromValue,\n\t\t\t\ttoValue: pending.toValue,\n\t\t\t\treason,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Remove a move from the pending map and, once none remain, allow\n\t\t * prop-driven rebuilds again.\n\t\t *\n\t\t * @param {unknown} objectId The moved object's `rowKey` value.\n\t\t * @return {void}\n\t\t */\n\t\tclearPending(objectId) {\n\t\t\tconst { [objectId]: _removed, ...rest } = this.pendingMoves\n\t\t\tthis.pendingMoves = rest\n\t\t\tif (Object.keys(rest).length === 0) {\n\t\t\t\tthis.suppressRebuild = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-object-kanban__loading,\n.cn-object-kanban__empty {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tpadding: 40px;\n}\n\n.cn-object-kanban__board {\n\tdisplay: flex;\n\tgap: 16px;\n\toverflow-x: auto;\n\tpadding-bottom: 8px;\n}\n\n.cn-object-kanban__column {\n\tdisplay: flex;\n\tflex-direction: column;\n\tflex: 0 0 280px;\n\tmin-width: 280px;\n\tbackground: var(--color-background-dark);\n\tborder-radius: var(--border-radius-large, 10px);\n\tpadding: 8px;\n}\n\n.cn-object-kanban__column-header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tpadding: 8px;\n\tfont-weight: 600;\n}\n\n.cn-object-kanban__column-count {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: 400;\n}\n\n.cn-object-kanban__column-cards {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-height: 40px;\n}\n\n.cn-object-kanban__card {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius, 8px);\n\tpadding: 10px;\n\tcursor: grab;\n\ttransition: opacity 0.15s ease;\n}\n\n.cn-object-kanban__card--pending {\n\topacity: 0.6;\n}\n\n/* A late card. The label carries the meaning; the edge helps a sighted\n reader find it. An inset shadow, so the card keeps its size. */\n.cn-object-kanban__card--overdue {\n\tborder-color: var(--color-error);\n\tbox-shadow: inset 3px 0 0 var(--color-error);\n}\n\n.cn-object-kanban__due {\n\tdisplay: block;\n\tfont-size: 0.85em;\n\tfont-weight: 700;\n}\n\n.cn-object-kanban__due--overdue {\n\tcolor: var(--color-error-text, var(--color-error));\n}\n\n.cn-object-kanban__due--soon {\n\tcolor: var(--color-warning-text, var(--color-main-text));\n\tfont-weight: 600;\n}\n\n.cn-object-kanban__card-title {\n\tfont-weight: 500;\n\tcursor: pointer;\n}\n\n.cn-object-kanban__card-fields {\n\tmargin-top: 6px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-object-kanban__load-more {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding-top: 8px;\n}\n</style>\n"],"names":["CnCellRenderer","t"],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ const _hoisted_6 = { class: "cn-object-kanban__column-title" };
|
|
|
18
18
|
const _hoisted_7 = { class: "cn-object-kanban__column-count" };
|
|
19
19
|
const _hoisted_8 = ["onClick"];
|
|
20
20
|
const _hoisted_9 = {
|
|
21
|
-
key:
|
|
21
|
+
key: 1,
|
|
22
22
|
class: "cn-object-kanban__card-fields"
|
|
23
23
|
};
|
|
24
24
|
const _hoisted_10 = {
|
|
@@ -74,12 +74,24 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
74
74
|
}, {
|
|
75
75
|
item: withCtx(({ element: card }) => [
|
|
76
76
|
createElementVNode("div", {
|
|
77
|
-
class: normalizeClass(["cn-object-kanban__card", {
|
|
77
|
+
class: normalizeClass(["cn-object-kanban__card", {
|
|
78
|
+
'cn-object-kanban__card--pending': $options.isPending(card),
|
|
79
|
+
'cn-object-kanban__card--overdue': $options.dueState(card) === 'overdue',
|
|
80
|
+
'cn-object-kanban__card--soon': $options.dueState(card) === 'soon',
|
|
81
|
+
}])
|
|
78
82
|
}, [
|
|
79
83
|
renderSlot(_ctx.$slots, "card", {
|
|
80
84
|
object: card,
|
|
81
|
-
column: column
|
|
85
|
+
column: column,
|
|
86
|
+
dueState: $options.dueState(card)
|
|
82
87
|
}, () => [
|
|
88
|
+
($options.dueLabel(card))
|
|
89
|
+
? (openBlock(), createElementBlock("span", {
|
|
90
|
+
key: 0,
|
|
91
|
+
class: normalizeClass(["cn-object-kanban__due", `cn-object-kanban__due--${$options.dueState(card)}`]),
|
|
92
|
+
"data-testid": "cn-kanban-due"
|
|
93
|
+
}, toDisplayString($options.dueLabel(card)), 3 /* TEXT, CLASS */))
|
|
94
|
+
: createCommentVNode("v-if", true),
|
|
83
95
|
createElementVNode("div", {
|
|
84
96
|
class: "cn-object-kanban__card-title",
|
|
85
97
|
onClick: $event => ($options.onCardClick(card))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnObjectKanban.vue3.js","sources":["../../../../src/components/CnObjectKanban/CnObjectKanban.vue?vue&type=template&id=3523a46f&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-object-kanban\">\n\t\t<div v-if=\"loading\" class=\"cn-object-kanban__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<div v-else-if=\"localColumns.length === 0\" class=\"cn-object-kanban__empty\">\n\t\t\t<!-- @slot empty Custom empty state shown when there are no columns to render. -->\n\t\t\t<slot name=\"empty\">\n\t\t\t\t<NcEmptyContent :name=\"emptyText\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<ViewColumn :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<div v-else class=\"cn-object-kanban__board\">\n\t\t\t<div\n\t\t\t\tv-for=\"column in localColumns\"\n\t\t\t\t:key=\"columnKey(column.value)\"\n\t\t\t\tclass=\"cn-object-kanban__column\">\n\t\t\t\t<div class=\"cn-object-kanban__column-header\">\n\t\t\t\t\t<!-- @slot column-header Override a column's header. -->\n\t\t\t\t\t<!-- @binding {object} column The column ({ value, cards, total, limit, offset }). -->\n\t\t\t\t\t<slot name=\"column-header\" :column=\"column\">\n\t\t\t\t\t\t<span class=\"cn-object-kanban__column-title\">{{ columnLabel(column) }}</span>\n\t\t\t\t\t\t<span class=\"cn-object-kanban__column-count\">{{ column.total }}</span>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- `#item` + `item-key`: vuedraggable@4 (the Vue 3 build)\n\t\t\t\t renders the cards itself from this slot. A `v-for` in the\n\t\t\t\t default slot throws \"draggable element must have an item\n\t\t\t\t slot\" the moment the column paints. -->\n\t\t\t\t<Draggable\n\t\t\t\t\t:list=\"column.cards\"\n\t\t\t\t\ttag=\"div\"\n\t\t\t\t\tclass=\"cn-object-kanban__column-cards\"\n\t\t\t\t\t:itemKey=\"cardKey\"\n\t\t\t\t\tgroup=\"cn-object-kanban-cards\"\n\t\t\t\t\t:data-column-value=\"columnKey(column.value)\"\n\t\t\t\t\t@start=\"onDragStart(column)\"\n\t\t\t\t\t@change=\"onColumnChange($event, column)\">\n\t\t\t\t\t<template #item=\"{ element: card }\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass=\"cn-object-kanban__card\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-object-kanban__card--pending': isPending(card) }\">\n\t\t\t\t\t\t\t<!-- @slot card Fully replace the default card rendering. -->\n\t\t\t\t\t\t\t<!-- @binding {object} object The card's object. -->\n\t\t\t\t\t\t\t<!-- @binding {object} column The column the card currently sits in. -->\n\t\t\t\t\t\t\t<slot name=\"card\" :object=\"card\" :column=\"column\">\n\t\t\t\t\t\t\t\t<div class=\"cn-object-kanban__card-title\" @click=\"onCardClick(card)\">\n\t\t\t\t\t\t\t\t\t{{ cardTitle(card) }}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div v-if=\"cardFields.length\" class=\"cn-object-kanban__card-fields\">\n\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\tv-for=\"field in cardFields\"\n\t\t\t\t\t\t\t\t\t\t:key=\"field\"\n\t\t\t\t\t\t\t\t\t\tclass=\"cn-object-kanban__card-field\">\n\t\t\t\t\t\t\t\t\t\t<CnCellRenderer\n\t\t\t\t\t\t\t\t\t\t\t:value=\"card[field]\"\n\t\t\t\t\t\t\t\t\t\t\t:property=\"schemaProperty(field)\"\n\t\t\t\t\t\t\t\t\t\t\t:truncate=\"40\" />\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</slot>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</template>\n\t\t\t\t</Draggable>\n\n\t\t\t\t<div v-if=\"hasMore(column)\" class=\"cn-object-kanban__load-more\">\n\t\t\t\t\t<NcButton :disabled=\"isColumnLoading(column)\" @click=\"onLoadMore(column)\">\n\t\t\t\t\t\t<template v-if=\"isColumnLoading(column)\" #icon>\n\t\t\t\t\t\t\t<NcLoadingIcon :size=\"16\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ loadMoreLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport draggable from 'vuedraggable'\nimport ViewColumn from 'vue-material-design-icons/ViewColumn.vue'\nimport { CnCellRenderer } from '../CnCellRenderer/index.js'\n\n/**\n * CnObjectKanban — Kanban board over a schema property's distinct values.\n *\n * Renders one column per distinct value of `groupByField` and paginates each\n * column's cards independently (\"load more\", never a whole column at once).\n * Column values/order come from (in precedence order) an explicit\n * `columnOrder`, the `groupByField` schema property's `enum`, or values\n * discovered from `objects` — mirroring OpenRegister's\n * `ViewPresentationService::deriveColumnValues()`.\n *\n * Two rendering modes:\n * - **Pre-built columns** — pass `columns` (the shape returned by\n * `GET /api/views/{id}/kanban`: `[{ value, cards, total, limit, offset }]`);\n * the host owns pagination and re-fetches on `load-more`.\n * - **Flat objects** — pass `objects` + `groupByField` (and optionally\n * `columnOrder`/`schema`); the component derives and paginates columns\n * locally.\n *\n * Dragging a card to another column does NOT write anything itself — there is\n * deliberately no bespoke \"move card\" endpoint (REQ-VIEW-KANBAN-03). The move\n * is applied optimistically to the local board and the component emits\n * `move`; the host performs the actual object PATCH/PUT through the existing\n * guarded write path and reports back:\n * - On success, nothing further is required (the optimistic move already\n * reflects it) — optionally call `resolveMove(id)` to clear the pending\n * visual state sooner.\n * - On a rejected (e.g. illegal lifecycle transition) write, call\n * `rejectMove(id, reason)` — the card snaps back to its origin column and\n * `move-rejected` fires with the reason for the host to surface.\n *\n * ```vue\n * <CnObjectKanban\n * :objects=\"objects\"\n * group-by-field=\"status\"\n * :column-order=\"['todo', 'doing', 'done']\"\n * :card-fields=\"['title', 'assignee']\"\n * :schema=\"schema\"\n * ref=\"kanban\"\n * @move=\"onMove\"\n * @load-more=\"fetchMoreCards\" />\n * ```\n * ```js\n * async onMove({ object, groupByField, fromValue, toValue }) {\n * try {\n * await patchObject(object.id, { [groupByField]: toValue })\n * } catch (e) {\n * this.$refs.kanban.rejectMove(object.id, e.message)\n * }\n * }\n * ```\n */\nexport default {\n\tname: 'CnObjectKanban',\n\n\tcomponents: {\n\t\tDraggable: draggable,\n\t\tNcButton,\n\t\tNcLoadingIcon,\n\t\tNcEmptyContent,\n\t\tViewColumn,\n\t\tCnCellRenderer,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Flat objects to derive columns from (ignored when `columns` is set).\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tobjects: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Pre-built columns, e.g. the response of `GET /api/views/{id}/kanban`.\n\t\t *\n\t\t * @type {Array<{value: string|number, cards: Array<object>, total: number, limit: number, offset: number}>|null}\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The schema property whose distinct values become columns. */\n\t\tgroupByField: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Explicit column order. Takes precedence over the schema's enum order. */\n\t\tcolumnOrder: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Object fields rendered on each card (in order). */\n\t\tcardFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Schema definition, used to resolve enum column order and card field types. */\n\t\tschema: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Overall loading state (initial board fetch). */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Column values currently loading more cards (drives the per-column spinner). */\n\t\tloadingColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Cards shown per column before \"load more\", in local (`objects`-driven) mode. */\n\t\tpageSize: {\n\t\t\ttype: Number,\n\t\t\tdefault: 20,\n\t\t},\n\n\t\t/** Object property used as each card's identity. */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\t},\n\n\temits: ['card-click', 'load-more', 'move', 'move-rejected'],\n\n\tdata() {\n\t\treturn {\n\t\t\tlocalColumns: [],\n\t\t\t// column.value -> full unpaginated card list, local mode only.\n\t\t\tfullCardsByColumn: {},\n\t\t\t// Suppressed while a move is pending so the prop-driven rebuild\n\t\t\t// doesn't clobber the optimistic UI before the host's data catches up.\n\t\t\tsuppressRebuild: false,\n\t\t\t// objectId -> { fromValue, toValue, card, originIndex }\n\t\t\tpendingMoves: {},\n\t\t\tdragOriginValue: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\temptyText() {\n\t\t\treturn t('nextcloud-vue', 'No columns to show')\n\t\t},\n\n\t\tloadMoreLabel() {\n\t\t\treturn t('nextcloud-vue', 'Load more')\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjects: {\n\t\t\thandler() {\n\t\t\t\tif (!this.suppressRebuild) {\n\t\t\t\t\tthis.rebuild()\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\tcolumns: {\n\t\t\thandler() {\n\t\t\t\tif (!this.suppressRebuild) {\n\t\t\t\t\tthis.rebuild()\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\tgroupByField() {\n\t\t\tthis.rebuild()\n\t\t},\n\n\t\tcolumnOrder: {\n\t\t\thandler() {\n\t\t\t\tthis.rebuild()\n\t\t\t},\n\n\t\t\tdeep: true,\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.rebuild()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Rebuild `localColumns` from either the `columns` prop (backend-paginated\n\t\t * mode) or `objects` + `groupByField` (locally-derived mode).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\trebuild() {\n\t\t\tif (Array.isArray(this.columns)) {\n\t\t\t\tthis.localColumns = this.columns.map((column) => ({\n\t\t\t\t\tvalue: column.value,\n\t\t\t\t\tcards: Array.isArray(column.cards) ? [...column.cards] : [],\n\t\t\t\t\ttotal: typeof column.total === 'number' ? column.total : (column.cards || []).length,\n\t\t\t\t\tlimit: column.limit,\n\t\t\t\t\toffset: column.offset,\n\t\t\t\t}))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst values = this.deriveColumnValues()\n\t\t\tconst fullByColumn = {}\n\t\t\tthis.localColumns = values.map((value) => {\n\t\t\t\tconst fullCards = this.objects.filter((object) => object && object[this.groupByField] === value)\n\t\t\t\tfullByColumn[this.columnKey(value)] = fullCards\n\t\t\t\treturn {\n\t\t\t\t\tvalue,\n\t\t\t\t\tcards: fullCards.slice(0, this.pageSize),\n\t\t\t\t\ttotal: fullCards.length,\n\t\t\t\t\tlimit: this.pageSize,\n\t\t\t\t\toffset: 0,\n\t\t\t\t}\n\t\t\t})\n\t\t\tthis.fullCardsByColumn = fullByColumn\n\t\t},\n\n\t\t/**\n\t\t * Derive the ordered column values: `columnOrder` > schema enum order >\n\t\t * distinct values observed in `objects`. Mirrors\n\t\t * `ViewPresentationService::deriveColumnValues()`.\n\t\t *\n\t\t * @return {Array<unknown>}\n\t\t */\n\t\tderiveColumnValues() {\n\t\t\tif (Array.isArray(this.columnOrder) && this.columnOrder.length > 0) {\n\t\t\t\treturn [...this.columnOrder]\n\t\t\t}\n\n\t\t\tconst enumValues = this.schema?.properties?.[this.groupByField]?.enum\n\t\t\tif (Array.isArray(enumValues) && enumValues.length > 0) {\n\t\t\t\treturn [...enumValues]\n\t\t\t}\n\n\t\t\treturn this.discoverDistinctValues()\n\t\t},\n\n\t\t/**\n\t\t * Distinct `groupByField` values observed in `objects`, in first-seen order.\n\t\t *\n\t\t * @return {Array<unknown>}\n\t\t */\n\t\tdiscoverDistinctValues() {\n\t\t\tconst seen = new Set()\n\t\t\tconst values = []\n\t\t\tfor (const object of this.objects) {\n\t\t\t\tconst value = object?.[this.groupByField]\n\t\t\t\tif (value !== undefined && value !== null && !seen.has(value)) {\n\t\t\t\t\tseen.add(value)\n\t\t\t\t\tvalues.push(value)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn values\n\t\t},\n\n\t\t/**\n\t\t * Stable string key for a column value (used as a `:key` and as the\n\t\t * `fullCardsByColumn` index — object/array values are unlikely but guarded).\n\t\t *\n\t\t * @param {unknown} value The column value.\n\t\t * @return {string}\n\t\t */\n\t\tcolumnKey(value) {\n\t\t\treturn typeof value === 'string' || typeof value === 'number' ? String(value) : JSON.stringify(value)\n\t\t},\n\n\t\t/**\n\t\t * The card's identity, per `rowKey`.\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {unknown}\n\t\t */\n\t\tcardKey(card) {\n\t\t\treturn card?.[this.rowKey]\n\t\t},\n\n\t\t/**\n\t\t * Display label for a column: its schema `oneOf[].title` when the\n\t\t * property declares per-value titles, otherwise the raw value.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {string}\n\t\t */\n\t\tcolumnLabel(column) {\n\t\t\tconst oneOf = this.schema?.properties?.[this.groupByField]?.oneOf\n\t\t\tif (Array.isArray(oneOf)) {\n\t\t\t\tconst match = oneOf.find((entry) => entry?.const === column.value)\n\t\t\t\tif (match?.title) {\n\t\t\t\t\treturn match.title\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn String(column.value)\n\t\t},\n\n\t\t/**\n\t\t * Best-effort card title: the schema's configured name field, else\n\t\t * `title`/`name`/the row key.\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {string}\n\t\t */\n\t\tcardTitle(card) {\n\t\t\tconst nameField = this.schema?.configuration?.objectNameField\n\t\t\tif (nameField && card[nameField]) {\n\t\t\t\treturn String(card[nameField])\n\t\t\t}\n\t\t\treturn String(card.title || card.name || card[this.rowKey] || '—')\n\t\t},\n\n\t\t/**\n\t\t * The schema property definition for a card field, if known.\n\t\t *\n\t\t * @param {string} field The field name.\n\t\t * @return {object|undefined}\n\t\t */\n\t\tschemaProperty(field) {\n\t\t\treturn this.schema?.properties?.[field]\n\t\t},\n\n\t\t/**\n\t\t * Whether a column has more cards to load: backend-paginated mode compares\n\t\t * `cards.length` to `total`; local mode compares to the full filtered set.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {boolean}\n\t\t */\n\t\thasMore(column) {\n\t\t\tif (Array.isArray(this.columns)) {\n\t\t\t\treturn column.cards.length < column.total\n\t\t\t}\n\t\t\tconst full = this.fullCardsByColumn[this.columnKey(column.value)] || []\n\t\t\treturn column.cards.length < full.length\n\t\t},\n\n\t\t/**\n\t\t * Whether a column's \"load more\" is in flight.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {boolean}\n\t\t */\n\t\tisColumnLoading(column) {\n\t\t\treturn this.loadingColumns.includes(column.value)\n\t\t},\n\n\t\t/**\n\t\t * Whether a card has an unresolved optimistic move.\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {boolean}\n\t\t */\n\t\tisPending(card) {\n\t\t\treturn Object.hasOwn(this.pendingMoves, this.cardKey(card))\n\t\t},\n\n\t\t/**\n\t\t * Handle a column's \"load more\" click: local mode reveals the next page\n\t\t * from the already-fetched full set; backend-paginated mode only emits —\n\t\t * the host fetches the next page and passes updated `columns`.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {void}\n\t\t */\n\t\tonLoadMore(column) {\n\t\t\tif (!Array.isArray(this.columns)) {\n\t\t\t\tconst full = this.fullCardsByColumn[this.columnKey(column.value)] || []\n\t\t\t\tcolumn.cards = full.slice(0, column.cards.length + this.pageSize)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event load-more Emitted when a column's \"load more\" is clicked.\n\t\t\t * @type {{ value: unknown, offset: number }}\n\t\t\t */\n\t\t\tthis.$emit('load-more', { value: column.value, offset: column.cards.length })\n\t\t},\n\n\t\t/**\n\t\t * Card click (ignored while the card is mid-drag).\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {void}\n\t\t */\n\t\tonCardClick(card) {\n\t\t\t/**\n\t\t\t * @event card-click Emitted when a card is clicked (not dragged).\n\t\t\t * @type {object} The card's object.\n\t\t\t */\n\t\t\tthis.$emit('card-click', card)\n\t\t},\n\n\t\t/**\n\t\t * vuedraggable `@start` — remember which column a drag began in, since\n\t\t * `@change` on the destination column doesn't know the source.\n\t\t *\n\t\t * @param {object} column The column the drag started in.\n\t\t * @return {void}\n\t\t */\n\t\tonDragStart(column) {\n\t\t\tthis.dragOriginValue = column.value\n\t\t},\n\n\t\t/**\n\t\t * vuedraggable `@change` on a column's card list. Only the destination\n\t\t * column's `added` event carries the dropped object; a same-column\n\t\t * reorder (`moved`) or the source column's `removed` are no-ops here.\n\t\t *\n\t\t * @param {object} evt vuedraggable's change event.\n\t\t * @param {object} column The column the event fired on.\n\t\t * @return {void}\n\t\t */\n\t\tonColumnChange(evt, column) {\n\t\t\tif (!evt.added) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst card = evt.added.element\n\t\t\tconst fromValue = this.dragOriginValue\n\t\t\tconst toValue = column.value\n\t\t\tthis.dragOriginValue = null\n\n\t\t\tif (fromValue === toValue) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.commitMove(card, fromValue, toValue, evt.added.newIndex)\n\t\t},\n\n\t\t/**\n\t\t * Record the pending move and emit it for the host to persist. The card\n\t\t * has already been moved optimistically by vuedraggable's v-model splice.\n\t\t *\n\t\t * @param {object} card The moved object.\n\t\t * @param {unknown} fromValue The origin column's value.\n\t\t * @param {unknown} toValue The destination column's value.\n\t\t * @param {number} originIndex The index the card is currently at in the destination column (used only for logging/debugging).\n\t\t * @return {void}\n\t\t */\n\t\tcommitMove(card, fromValue, toValue, originIndex) {\n\t\t\tconst id = this.cardKey(card)\n\t\t\tthis.suppressRebuild = true\n\t\t\tthis.pendingMoves = {\n\t\t\t\t...this.pendingMoves,\n\t\t\t\t[id]: { fromValue, toValue, card, originIndex },\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @event move Emitted after a card is optimistically moved to another\n\t\t\t * column. The host performs the actual object write through the\n\t\t\t * existing guarded PATCH/PUT endpoint and calls `resolveMove`/\n\t\t\t * `rejectMove` on this component to confirm or roll back.\n\t\t\t * @type {{ object: object, groupByField: string, fromValue: unknown, toValue: unknown }}\n\t\t\t */\n\t\t\tthis.$emit('move', {\n\t\t\t\tobject: card,\n\t\t\t\tgroupByField: this.groupByField,\n\t\t\t\tfromValue,\n\t\t\t\ttoValue,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Confirm a pending move succeeded — clears its pending visual state.\n\t\t * The card already sits in the destination column (optimistic UI); no\n\t\t * further mutation is needed.\n\t\t *\n\t\t * @param {unknown} objectId The moved object's `rowKey` value.\n\t\t * @return {void}\n\t\t * @public\n\t\t */\n\t\tresolveMove(objectId) {\n\t\t\tthis.clearPending(objectId)\n\t\t},\n\n\t\t/**\n\t\t * Roll back a rejected move: the card returns to its origin column and\n\t\t * `move-rejected` fires with the server's reason.\n\t\t *\n\t\t * @param {unknown} objectId The moved object's `rowKey` value.\n\t\t * @param {string} [reason] The rejection reason (e.g. an illegal lifecycle\n\t\t * transition message) to surface to the user.\n\t\t * @return {void}\n\t\t * @public\n\t\t */\n\t\trejectMove(objectId, reason) {\n\t\t\tconst pending = this.pendingMoves[objectId]\n\t\t\tif (!pending) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst toColumn = this.localColumns.find((c) => c.value === pending.toValue)\n\t\t\tconst fromColumn = this.localColumns.find((c) => c.value === pending.fromValue)\n\n\t\t\tif (toColumn) {\n\t\t\t\tconst idx = toColumn.cards.findIndex((c) => this.cardKey(c) === objectId)\n\t\t\t\tif (idx !== -1) {\n\t\t\t\t\ttoColumn.cards.splice(idx, 1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (fromColumn) {\n\t\t\t\tfromColumn.cards.push(pending.card)\n\t\t\t}\n\n\t\t\tthis.clearPending(objectId)\n\n\t\t\t/**\n\t\t\t * @event move-rejected Emitted after `rejectMove()` rolls a card back\n\t\t\t * to its origin column.\n\t\t\t * @type {{ object: object, fromValue: unknown, toValue: unknown, reason: (string|undefined) }}\n\t\t\t */\n\t\t\tthis.$emit('move-rejected', {\n\t\t\t\tobject: pending.card,\n\t\t\t\tfromValue: pending.fromValue,\n\t\t\t\ttoValue: pending.toValue,\n\t\t\t\treason,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Remove a move from the pending map and, once none remain, allow\n\t\t * prop-driven rebuilds again.\n\t\t *\n\t\t * @param {unknown} objectId The moved object's `rowKey` value.\n\t\t * @return {void}\n\t\t */\n\t\tclearPending(objectId) {\n\t\t\tconst { [objectId]: _removed, ...rest } = this.pendingMoves\n\t\t\tthis.pendingMoves = rest\n\t\t\tif (Object.keys(rest).length === 0) {\n\t\t\t\tthis.suppressRebuild = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-object-kanban__loading,\n.cn-object-kanban__empty {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tpadding: 40px;\n}\n\n.cn-object-kanban__board {\n\tdisplay: flex;\n\tgap: 16px;\n\toverflow-x: auto;\n\tpadding-bottom: 8px;\n}\n\n.cn-object-kanban__column {\n\tdisplay: flex;\n\tflex-direction: column;\n\tflex: 0 0 280px;\n\tmin-width: 280px;\n\tbackground: var(--color-background-dark);\n\tborder-radius: var(--border-radius-large, 10px);\n\tpadding: 8px;\n}\n\n.cn-object-kanban__column-header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tpadding: 8px;\n\tfont-weight: 600;\n}\n\n.cn-object-kanban__column-count {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: 400;\n}\n\n.cn-object-kanban__column-cards {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-height: 40px;\n}\n\n.cn-object-kanban__card {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius, 8px);\n\tpadding: 10px;\n\tcursor: grab;\n\ttransition: opacity 0.15s ease;\n}\n\n.cn-object-kanban__card--pending {\n\topacity: 0.6;\n}\n\n.cn-object-kanban__card-title {\n\tfont-weight: 500;\n\tcursor: pointer;\n}\n\n.cn-object-kanban__card-fields {\n\tmargin-top: 6px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-object-kanban__load-more {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding-top: 8px;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createVNode","_renderSlot","_createElementVNode","_toDisplayString","_withCtx","_normalizeClass","_Fragment","_renderList","_createTextVNode"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnObjectKanban.vue3.js","sources":["../../../../src/components/CnObjectKanban/CnObjectKanban.vue?vue&type=template&id=3523a46f&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-object-kanban\">\n\t\t<div v-if=\"loading\" class=\"cn-object-kanban__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<div v-else-if=\"localColumns.length === 0\" class=\"cn-object-kanban__empty\">\n\t\t\t<!-- @slot empty Custom empty state shown when there are no columns to render. -->\n\t\t\t<slot name=\"empty\">\n\t\t\t\t<NcEmptyContent :name=\"emptyText\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<ViewColumn :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<div v-else class=\"cn-object-kanban__board\">\n\t\t\t<div\n\t\t\t\tv-for=\"column in localColumns\"\n\t\t\t\t:key=\"columnKey(column.value)\"\n\t\t\t\tclass=\"cn-object-kanban__column\">\n\t\t\t\t<div class=\"cn-object-kanban__column-header\">\n\t\t\t\t\t<!-- @slot column-header Override a column's header. -->\n\t\t\t\t\t<!-- @binding {object} column The column ({ value, cards, total, limit, offset }). -->\n\t\t\t\t\t<slot name=\"column-header\" :column=\"column\">\n\t\t\t\t\t\t<span class=\"cn-object-kanban__column-title\">{{ columnLabel(column) }}</span>\n\t\t\t\t\t\t<span class=\"cn-object-kanban__column-count\">{{ column.total }}</span>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- `#item` + `item-key`: vuedraggable@4 (the Vue 3 build)\n\t\t\t\t renders the cards itself from this slot. A `v-for` in the\n\t\t\t\t default slot throws \"draggable element must have an item\n\t\t\t\t slot\" the moment the column paints. -->\n\t\t\t\t<Draggable\n\t\t\t\t\t:list=\"column.cards\"\n\t\t\t\t\ttag=\"div\"\n\t\t\t\t\tclass=\"cn-object-kanban__column-cards\"\n\t\t\t\t\t:itemKey=\"cardKey\"\n\t\t\t\t\tgroup=\"cn-object-kanban-cards\"\n\t\t\t\t\t:data-column-value=\"columnKey(column.value)\"\n\t\t\t\t\t@start=\"onDragStart(column)\"\n\t\t\t\t\t@change=\"onColumnChange($event, column)\">\n\t\t\t\t\t<template #item=\"{ element: card }\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass=\"cn-object-kanban__card\"\n\t\t\t\t\t\t\t:class=\"{\n\t\t\t\t\t\t\t\t'cn-object-kanban__card--pending': isPending(card),\n\t\t\t\t\t\t\t\t'cn-object-kanban__card--overdue': dueState(card) === 'overdue',\n\t\t\t\t\t\t\t\t'cn-object-kanban__card--soon': dueState(card) === 'soon',\n\t\t\t\t\t\t\t}\">\n\t\t\t\t\t\t\t<!-- @slot card Fully replace the default card rendering. -->\n\t\t\t\t\t\t\t<!-- @binding {object} object The card's object. -->\n\t\t\t\t\t\t\t<!-- @binding {object} column The column the card currently sits in. -->\n\t\t\t\t\t\t\t<!-- @binding {string|null} dueState The card's due state under `dueRule`: `overdue`, `soon`, `ok`, or null without a rule. -->\n\t\t\t\t\t\t\t<slot\n\t\t\t\t\t\t\t\tname=\"card\"\n\t\t\t\t\t\t\t\t:object=\"card\"\n\t\t\t\t\t\t\t\t:column=\"column\"\n\t\t\t\t\t\t\t\t:dueState=\"dueState(card)\">\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"dueLabel(card)\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-object-kanban__due\"\n\t\t\t\t\t\t\t\t\t:class=\"`cn-object-kanban__due--${dueState(card)}`\"\n\t\t\t\t\t\t\t\t\tdata-testid=\"cn-kanban-due\">\n\t\t\t\t\t\t\t\t\t{{ dueLabel(card) }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<div class=\"cn-object-kanban__card-title\" @click=\"onCardClick(card)\">\n\t\t\t\t\t\t\t\t\t{{ cardTitle(card) }}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div v-if=\"cardFields.length\" class=\"cn-object-kanban__card-fields\">\n\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\tv-for=\"field in cardFields\"\n\t\t\t\t\t\t\t\t\t\t:key=\"field\"\n\t\t\t\t\t\t\t\t\t\tclass=\"cn-object-kanban__card-field\">\n\t\t\t\t\t\t\t\t\t\t<CnCellRenderer\n\t\t\t\t\t\t\t\t\t\t\t:value=\"card[field]\"\n\t\t\t\t\t\t\t\t\t\t\t:property=\"schemaProperty(field)\"\n\t\t\t\t\t\t\t\t\t\t\t:truncate=\"40\" />\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</slot>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</template>\n\t\t\t\t</Draggable>\n\n\t\t\t\t<div v-if=\"hasMore(column)\" class=\"cn-object-kanban__load-more\">\n\t\t\t\t\t<NcButton :disabled=\"isColumnLoading(column)\" @click=\"onLoadMore(column)\">\n\t\t\t\t\t\t<template v-if=\"isColumnLoading(column)\" #icon>\n\t\t\t\t\t\t\t<NcLoadingIcon :size=\"16\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ loadMoreLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport draggable from 'vuedraggable'\nimport ViewColumn from 'vue-material-design-icons/ViewColumn.vue'\nimport { dueStateForRow } from '../../utils/dueRule.js'\nimport { CnCellRenderer } from '../CnCellRenderer/index.js'\n\n/**\n * CnObjectKanban — Kanban board over a schema property's distinct values.\n *\n * Renders one column per distinct value of `groupByField` and paginates each\n * column's cards independently (\"load more\", never a whole column at once).\n * Column values/order come from (in precedence order) an explicit\n * `columnOrder`, the `groupByField` schema property's `enum`, or values\n * discovered from `objects` — mirroring OpenRegister's\n * `ViewPresentationService::deriveColumnValues()`.\n *\n * Two rendering modes:\n * - **Pre-built columns** — pass `columns` (the shape returned by\n * `GET /api/views/{id}/kanban`: `[{ value, cards, total, limit, offset }]`);\n * the host owns pagination and re-fetches on `load-more`.\n * - **Flat objects** — pass `objects` + `groupByField` (and optionally\n * `columnOrder`/`schema`); the component derives and paginates columns\n * locally.\n *\n * Dragging a card to another column does NOT write anything itself — there is\n * deliberately no bespoke \"move card\" endpoint (REQ-VIEW-KANBAN-03). The move\n * is applied optimistically to the local board and the component emits\n * `move`; the host performs the actual object PATCH/PUT through the existing\n * guarded write path and reports back:\n * - On success, nothing further is required (the optimistic move already\n * reflects it) — optionally call `resolveMove(id)` to clear the pending\n * visual state sooner.\n * - On a rejected (e.g. illegal lifecycle transition) write, call\n * `rejectMove(id, reason)` — the card snaps back to its origin column and\n * `move-rejected` fires with the reason for the host to surface.\n *\n * ```vue\n * <CnObjectKanban\n * :objects=\"objects\"\n * group-by-field=\"status\"\n * :column-order=\"['todo', 'doing', 'done']\"\n * :card-fields=\"['title', 'assignee']\"\n * :schema=\"schema\"\n * ref=\"kanban\"\n * @move=\"onMove\"\n * @load-more=\"fetchMoreCards\" />\n * ```\n * ```js\n * async onMove({ object, groupByField, fromValue, toValue }) {\n * try {\n * await patchObject(object.id, { [groupByField]: toValue })\n * } catch (e) {\n * this.$refs.kanban.rejectMove(object.id, e.message)\n * }\n * }\n * ```\n */\nexport default {\n\tname: 'CnObjectKanban',\n\n\tcomponents: {\n\t\tDraggable: draggable,\n\t\tNcButton,\n\t\tNcLoadingIcon,\n\t\tNcEmptyContent,\n\t\tViewColumn,\n\t\tCnCellRenderer,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Flat objects to derive columns from (ignored when `columns` is set).\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tobjects: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Pre-built columns, e.g. the response of `GET /api/views/{id}/kanban`.\n\t\t *\n\t\t * @type {Array<{value: string|number, cards: Array<object>, total: number, limit: number, offset: number}>|null}\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The schema property whose distinct values become columns. */\n\t\tgroupByField: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Explicit column order. Takes precedence over the schema's enum order. */\n\t\tcolumnOrder: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Object fields rendered on each card (in order). */\n\t\tcardFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Schema definition, used to resolve enum column order and card field types. */\n\t\tschema: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Overall loading state (initial board fetch). */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Column values currently loading more cards (drives the per-column spinner). */\n\t\tloadingColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Cards shown per column before \"load more\", in local (`objects`-driven) mode. */\n\t\tpageSize: {\n\t\t\ttype: Number,\n\t\t\tdefault: 20,\n\t\t},\n\n\t\t/** Object property used as each card's identity. */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/**\n\t\t * Marks late cards: `{ field, soonDays? }`. `field` is the dot-path to\n\t\t * a card's due date. A date before today gets an error edge and the\n\t\t * label \"Overdue\"; a date within `soonDays` (default 3) gets \"Due\n\t\t * soon\" in the warning colour. `null` (the default) marks nothing.\n\t\t *\n\t\t * The same rule shape the table's date cell uses, so a list and its\n\t\t * board agree on what late means.\n\t\t *\n\t\t * @type {{field: string, soonDays?: number}|null}\n\t\t */\n\t\tdueRule: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['card-click', 'load-more', 'move', 'move-rejected'],\n\n\tdata() {\n\t\treturn {\n\t\t\tlocalColumns: [],\n\t\t\t// column.value -> full unpaginated card list, local mode only.\n\t\t\tfullCardsByColumn: {},\n\t\t\t// Suppressed while a move is pending so the prop-driven rebuild\n\t\t\t// doesn't clobber the optimistic UI before the host's data catches up.\n\t\t\tsuppressRebuild: false,\n\t\t\t// objectId -> { fromValue, toValue, card, originIndex }\n\t\t\tpendingMoves: {},\n\t\t\tdragOriginValue: null,\n\t\t\t// The day late marking measures from.\n\t\t\ttoday: new Date(),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\temptyText() {\n\t\t\treturn t('nextcloud-vue', 'No columns to show')\n\t\t},\n\n\t\tloadMoreLabel() {\n\t\t\treturn t('nextcloud-vue', 'Load more')\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjects: {\n\t\t\thandler() {\n\t\t\t\tif (!this.suppressRebuild) {\n\t\t\t\t\tthis.rebuild()\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\tcolumns: {\n\t\t\thandler() {\n\t\t\t\tif (!this.suppressRebuild) {\n\t\t\t\t\tthis.rebuild()\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tdeep: false,\n\t\t},\n\n\t\tgroupByField() {\n\t\t\tthis.rebuild()\n\t\t},\n\n\t\tcolumnOrder: {\n\t\t\thandler() {\n\t\t\t\tthis.rebuild()\n\t\t\t},\n\n\t\t\tdeep: true,\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.rebuild()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * A card's due state under `dueRule`.\n\t\t *\n\t\t * @param {object} card The card's row.\n\t\t * @return {'overdue'|'soon'|'ok'|null} The state, or null when no rule applies.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-late-marking-on-board-cards\n\t\t */\n\t\tdueState(card) {\n\t\t\treturn dueStateForRow(card, this.dueRule, this.today)\n\t\t},\n\n\t\t/**\n\t\t * The words a late card carries. Colour alone would not reach a reader\n\t\t * who cannot see it, or cannot tell red from amber.\n\t\t *\n\t\t * @param {object} card The card's row.\n\t\t * @return {string} \"Overdue\", \"Due soon\", or '' for a card that is fine.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-late-marking-on-board-cards\n\t\t */\n\t\tdueLabel(card) {\n\t\t\tconst state = this.dueState(card)\n\t\t\tif (state === 'overdue') {\n\t\t\t\treturn t('nextcloud-vue', 'Overdue')\n\t\t\t}\n\t\t\treturn state === 'soon' ? t('nextcloud-vue', 'Due soon') : ''\n\t\t},\n\n\t\t/**\n\t\t * Rebuild `localColumns` from either the `columns` prop (backend-paginated\n\t\t * mode) or `objects` + `groupByField` (locally-derived mode).\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\trebuild() {\n\t\t\tif (Array.isArray(this.columns)) {\n\t\t\t\tthis.localColumns = this.columns.map((column) => ({\n\t\t\t\t\tvalue: column.value,\n\t\t\t\t\tcards: Array.isArray(column.cards) ? [...column.cards] : [],\n\t\t\t\t\ttotal: typeof column.total === 'number' ? column.total : (column.cards || []).length,\n\t\t\t\t\tlimit: column.limit,\n\t\t\t\t\toffset: column.offset,\n\t\t\t\t}))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst values = this.deriveColumnValues()\n\t\t\tconst fullByColumn = {}\n\t\t\tthis.localColumns = values.map((value) => {\n\t\t\t\tconst fullCards = this.objects.filter((object) => object && object[this.groupByField] === value)\n\t\t\t\tfullByColumn[this.columnKey(value)] = fullCards\n\t\t\t\treturn {\n\t\t\t\t\tvalue,\n\t\t\t\t\tcards: fullCards.slice(0, this.pageSize),\n\t\t\t\t\ttotal: fullCards.length,\n\t\t\t\t\tlimit: this.pageSize,\n\t\t\t\t\toffset: 0,\n\t\t\t\t}\n\t\t\t})\n\t\t\tthis.fullCardsByColumn = fullByColumn\n\t\t},\n\n\t\t/**\n\t\t * Derive the ordered column values: `columnOrder` > schema enum order >\n\t\t * distinct values observed in `objects`. Mirrors\n\t\t * `ViewPresentationService::deriveColumnValues()`.\n\t\t *\n\t\t * @return {Array<unknown>}\n\t\t */\n\t\tderiveColumnValues() {\n\t\t\tif (Array.isArray(this.columnOrder) && this.columnOrder.length > 0) {\n\t\t\t\treturn [...this.columnOrder]\n\t\t\t}\n\n\t\t\tconst enumValues = this.schema?.properties?.[this.groupByField]?.enum\n\t\t\tif (Array.isArray(enumValues) && enumValues.length > 0) {\n\t\t\t\treturn [...enumValues]\n\t\t\t}\n\n\t\t\treturn this.discoverDistinctValues()\n\t\t},\n\n\t\t/**\n\t\t * Distinct `groupByField` values observed in `objects`, in first-seen order.\n\t\t *\n\t\t * @return {Array<unknown>}\n\t\t */\n\t\tdiscoverDistinctValues() {\n\t\t\tconst seen = new Set()\n\t\t\tconst values = []\n\t\t\tfor (const object of this.objects) {\n\t\t\t\tconst value = object?.[this.groupByField]\n\t\t\t\tif (value !== undefined && value !== null && !seen.has(value)) {\n\t\t\t\t\tseen.add(value)\n\t\t\t\t\tvalues.push(value)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn values\n\t\t},\n\n\t\t/**\n\t\t * Stable string key for a column value (used as a `:key` and as the\n\t\t * `fullCardsByColumn` index — object/array values are unlikely but guarded).\n\t\t *\n\t\t * @param {unknown} value The column value.\n\t\t * @return {string}\n\t\t */\n\t\tcolumnKey(value) {\n\t\t\treturn typeof value === 'string' || typeof value === 'number' ? String(value) : JSON.stringify(value)\n\t\t},\n\n\t\t/**\n\t\t * The card's identity, per `rowKey`.\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {unknown}\n\t\t */\n\t\tcardKey(card) {\n\t\t\treturn card?.[this.rowKey]\n\t\t},\n\n\t\t/**\n\t\t * Display label for a column: its schema `oneOf[].title` when the\n\t\t * property declares per-value titles, otherwise the raw value.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {string}\n\t\t */\n\t\tcolumnLabel(column) {\n\t\t\tconst oneOf = this.schema?.properties?.[this.groupByField]?.oneOf\n\t\t\tif (Array.isArray(oneOf)) {\n\t\t\t\tconst match = oneOf.find((entry) => entry?.const === column.value)\n\t\t\t\tif (match?.title) {\n\t\t\t\t\treturn match.title\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn String(column.value)\n\t\t},\n\n\t\t/**\n\t\t * Best-effort card title: the schema's configured name field, else\n\t\t * `title`/`name`/the row key.\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {string}\n\t\t */\n\t\tcardTitle(card) {\n\t\t\tconst nameField = this.schema?.configuration?.objectNameField\n\t\t\tif (nameField && card[nameField]) {\n\t\t\t\treturn String(card[nameField])\n\t\t\t}\n\t\t\treturn String(card.title || card.name || card[this.rowKey] || '—')\n\t\t},\n\n\t\t/**\n\t\t * The schema property definition for a card field, if known.\n\t\t *\n\t\t * @param {string} field The field name.\n\t\t * @return {object|undefined}\n\t\t */\n\t\tschemaProperty(field) {\n\t\t\treturn this.schema?.properties?.[field]\n\t\t},\n\n\t\t/**\n\t\t * Whether a column has more cards to load: backend-paginated mode compares\n\t\t * `cards.length` to `total`; local mode compares to the full filtered set.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {boolean}\n\t\t */\n\t\thasMore(column) {\n\t\t\tif (Array.isArray(this.columns)) {\n\t\t\t\treturn column.cards.length < column.total\n\t\t\t}\n\t\t\tconst full = this.fullCardsByColumn[this.columnKey(column.value)] || []\n\t\t\treturn column.cards.length < full.length\n\t\t},\n\n\t\t/**\n\t\t * Whether a column's \"load more\" is in flight.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {boolean}\n\t\t */\n\t\tisColumnLoading(column) {\n\t\t\treturn this.loadingColumns.includes(column.value)\n\t\t},\n\n\t\t/**\n\t\t * Whether a card has an unresolved optimistic move.\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {boolean}\n\t\t */\n\t\tisPending(card) {\n\t\t\treturn Object.hasOwn(this.pendingMoves, this.cardKey(card))\n\t\t},\n\n\t\t/**\n\t\t * Handle a column's \"load more\" click: local mode reveals the next page\n\t\t * from the already-fetched full set; backend-paginated mode only emits —\n\t\t * the host fetches the next page and passes updated `columns`.\n\t\t *\n\t\t * @param {object} column The column.\n\t\t * @return {void}\n\t\t */\n\t\tonLoadMore(column) {\n\t\t\tif (!Array.isArray(this.columns)) {\n\t\t\t\tconst full = this.fullCardsByColumn[this.columnKey(column.value)] || []\n\t\t\t\tcolumn.cards = full.slice(0, column.cards.length + this.pageSize)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event load-more Emitted when a column's \"load more\" is clicked.\n\t\t\t * @type {{ value: unknown, offset: number }}\n\t\t\t */\n\t\t\tthis.$emit('load-more', { value: column.value, offset: column.cards.length })\n\t\t},\n\n\t\t/**\n\t\t * Card click (ignored while the card is mid-drag).\n\t\t *\n\t\t * @param {object} card The card object.\n\t\t * @return {void}\n\t\t */\n\t\tonCardClick(card) {\n\t\t\t/**\n\t\t\t * @event card-click Emitted when a card is clicked (not dragged).\n\t\t\t * @type {object} The card's object.\n\t\t\t */\n\t\t\tthis.$emit('card-click', card)\n\t\t},\n\n\t\t/**\n\t\t * vuedraggable `@start` — remember which column a drag began in, since\n\t\t * `@change` on the destination column doesn't know the source.\n\t\t *\n\t\t * @param {object} column The column the drag started in.\n\t\t * @return {void}\n\t\t */\n\t\tonDragStart(column) {\n\t\t\tthis.dragOriginValue = column.value\n\t\t},\n\n\t\t/**\n\t\t * vuedraggable `@change` on a column's card list. Only the destination\n\t\t * column's `added` event carries the dropped object; a same-column\n\t\t * reorder (`moved`) or the source column's `removed` are no-ops here.\n\t\t *\n\t\t * @param {object} evt vuedraggable's change event.\n\t\t * @param {object} column The column the event fired on.\n\t\t * @return {void}\n\t\t */\n\t\tonColumnChange(evt, column) {\n\t\t\tif (!evt.added) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst card = evt.added.element\n\t\t\tconst fromValue = this.dragOriginValue\n\t\t\tconst toValue = column.value\n\t\t\tthis.dragOriginValue = null\n\n\t\t\tif (fromValue === toValue) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.commitMove(card, fromValue, toValue, evt.added.newIndex)\n\t\t},\n\n\t\t/**\n\t\t * Record the pending move and emit it for the host to persist. The card\n\t\t * has already been moved optimistically by vuedraggable's v-model splice.\n\t\t *\n\t\t * @param {object} card The moved object.\n\t\t * @param {unknown} fromValue The origin column's value.\n\t\t * @param {unknown} toValue The destination column's value.\n\t\t * @param {number} originIndex The index the card is currently at in the destination column (used only for logging/debugging).\n\t\t * @return {void}\n\t\t */\n\t\tcommitMove(card, fromValue, toValue, originIndex) {\n\t\t\tconst id = this.cardKey(card)\n\t\t\tthis.suppressRebuild = true\n\t\t\tthis.pendingMoves = {\n\t\t\t\t...this.pendingMoves,\n\t\t\t\t[id]: { fromValue, toValue, card, originIndex },\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @event move Emitted after a card is optimistically moved to another\n\t\t\t * column. The host performs the actual object write through the\n\t\t\t * existing guarded PATCH/PUT endpoint and calls `resolveMove`/\n\t\t\t * `rejectMove` on this component to confirm or roll back.\n\t\t\t * @type {{ object: object, groupByField: string, fromValue: unknown, toValue: unknown }}\n\t\t\t */\n\t\t\tthis.$emit('move', {\n\t\t\t\tobject: card,\n\t\t\t\tgroupByField: this.groupByField,\n\t\t\t\tfromValue,\n\t\t\t\ttoValue,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Confirm a pending move succeeded — clears its pending visual state.\n\t\t * The card already sits in the destination column (optimistic UI); no\n\t\t * further mutation is needed.\n\t\t *\n\t\t * @param {unknown} objectId The moved object's `rowKey` value.\n\t\t * @return {void}\n\t\t * @public\n\t\t */\n\t\tresolveMove(objectId) {\n\t\t\tthis.clearPending(objectId)\n\t\t},\n\n\t\t/**\n\t\t * Roll back a rejected move: the card returns to its origin column and\n\t\t * `move-rejected` fires with the server's reason.\n\t\t *\n\t\t * @param {unknown} objectId The moved object's `rowKey` value.\n\t\t * @param {string} [reason] The rejection reason (e.g. an illegal lifecycle\n\t\t * transition message) to surface to the user.\n\t\t * @return {void}\n\t\t * @public\n\t\t */\n\t\trejectMove(objectId, reason) {\n\t\t\tconst pending = this.pendingMoves[objectId]\n\t\t\tif (!pending) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst toColumn = this.localColumns.find((c) => c.value === pending.toValue)\n\t\t\tconst fromColumn = this.localColumns.find((c) => c.value === pending.fromValue)\n\n\t\t\tif (toColumn) {\n\t\t\t\tconst idx = toColumn.cards.findIndex((c) => this.cardKey(c) === objectId)\n\t\t\t\tif (idx !== -1) {\n\t\t\t\t\ttoColumn.cards.splice(idx, 1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (fromColumn) {\n\t\t\t\tfromColumn.cards.push(pending.card)\n\t\t\t}\n\n\t\t\tthis.clearPending(objectId)\n\n\t\t\t/**\n\t\t\t * @event move-rejected Emitted after `rejectMove()` rolls a card back\n\t\t\t * to its origin column.\n\t\t\t * @type {{ object: object, fromValue: unknown, toValue: unknown, reason: (string|undefined) }}\n\t\t\t */\n\t\t\tthis.$emit('move-rejected', {\n\t\t\t\tobject: pending.card,\n\t\t\t\tfromValue: pending.fromValue,\n\t\t\t\ttoValue: pending.toValue,\n\t\t\t\treason,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Remove a move from the pending map and, once none remain, allow\n\t\t * prop-driven rebuilds again.\n\t\t *\n\t\t * @param {unknown} objectId The moved object's `rowKey` value.\n\t\t * @return {void}\n\t\t */\n\t\tclearPending(objectId) {\n\t\t\tconst { [objectId]: _removed, ...rest } = this.pendingMoves\n\t\t\tthis.pendingMoves = rest\n\t\t\tif (Object.keys(rest).length === 0) {\n\t\t\t\tthis.suppressRebuild = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-object-kanban__loading,\n.cn-object-kanban__empty {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tpadding: 40px;\n}\n\n.cn-object-kanban__board {\n\tdisplay: flex;\n\tgap: 16px;\n\toverflow-x: auto;\n\tpadding-bottom: 8px;\n}\n\n.cn-object-kanban__column {\n\tdisplay: flex;\n\tflex-direction: column;\n\tflex: 0 0 280px;\n\tmin-width: 280px;\n\tbackground: var(--color-background-dark);\n\tborder-radius: var(--border-radius-large, 10px);\n\tpadding: 8px;\n}\n\n.cn-object-kanban__column-header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tpadding: 8px;\n\tfont-weight: 600;\n}\n\n.cn-object-kanban__column-count {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: 400;\n}\n\n.cn-object-kanban__column-cards {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-height: 40px;\n}\n\n.cn-object-kanban__card {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius, 8px);\n\tpadding: 10px;\n\tcursor: grab;\n\ttransition: opacity 0.15s ease;\n}\n\n.cn-object-kanban__card--pending {\n\topacity: 0.6;\n}\n\n/* A late card. The label carries the meaning; the edge helps a sighted\n reader find it. An inset shadow, so the card keeps its size. */\n.cn-object-kanban__card--overdue {\n\tborder-color: var(--color-error);\n\tbox-shadow: inset 3px 0 0 var(--color-error);\n}\n\n.cn-object-kanban__due {\n\tdisplay: block;\n\tfont-size: 0.85em;\n\tfont-weight: 700;\n}\n\n.cn-object-kanban__due--overdue {\n\tcolor: var(--color-error-text, var(--color-error));\n}\n\n.cn-object-kanban__due--soon {\n\tcolor: var(--color-warning-text, var(--color-main-text));\n\tfont-weight: 600;\n}\n\n.cn-object-kanban__card-title {\n\tfont-weight: 500;\n\tcursor: pointer;\n}\n\n.cn-object-kanban__card-fields {\n\tmargin-top: 6px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-object-kanban__load-more {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding-top: 8px;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createVNode","_renderSlot","_createElementVNode","_toDisplayString","_withCtx","_normalizeClass","_Fragment","_renderList","_createTextVNode"],"mappings":"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"}
|
|
@@ -125,6 +125,19 @@ var script = {
|
|
|
125
125
|
default: 0,
|
|
126
126
|
validator: (v) => Number.isFinite(v) && v >= 0,
|
|
127
127
|
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* How many records each tab matches, keyed by tab index (an array in
|
|
131
|
+
* tab order works too). A tab with a number here shows it after its
|
|
132
|
+
* label; a tab without one shows no count. A tab can also carry its
|
|
133
|
+
* own `count`. `null` (the default) shows no counts at all.
|
|
134
|
+
*
|
|
135
|
+
* @type {{[index: number]: number}|Array<number|null>|null}
|
|
136
|
+
*/
|
|
137
|
+
counts: {
|
|
138
|
+
type: [Object, Array],
|
|
139
|
+
default: null,
|
|
140
|
+
},
|
|
128
141
|
},
|
|
129
142
|
|
|
130
143
|
emits: ['update:active-index', 'update:selected-indices'],
|
|
@@ -218,10 +231,19 @@ var script = {
|
|
|
218
231
|
* omitted — an empty selection already means "all".
|
|
219
232
|
*
|
|
220
233
|
* @return {Array<{label: string, index: number, icon: string}>}
|
|
234
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views
|
|
221
235
|
*/
|
|
222
236
|
dropdownOptions() {
|
|
223
237
|
return this.tabs
|
|
224
|
-
.map((tab, index) =>
|
|
238
|
+
.map((tab, index) => {
|
|
239
|
+
const count = this.countOf({ tab, index });
|
|
240
|
+
return {
|
|
241
|
+
label: count === null ? tab.label : `${tab.label} (${count})`,
|
|
242
|
+
icon: tab.icon,
|
|
243
|
+
index,
|
|
244
|
+
_empty: this.isEmptyFilter(tab),
|
|
245
|
+
}
|
|
246
|
+
})
|
|
225
247
|
.filter((o) => !o._empty)
|
|
226
248
|
},
|
|
227
249
|
|
|
@@ -240,6 +262,20 @@ var script = {
|
|
|
240
262
|
},
|
|
241
263
|
|
|
242
264
|
methods: {
|
|
265
|
+
/**
|
|
266
|
+
* The count to show on a tab, or null for none: `counts[index]`
|
|
267
|
+
* first, else the tab's own `count`.
|
|
268
|
+
*
|
|
269
|
+
* @param {{tab: object, index: number}} entry The tab and its index.
|
|
270
|
+
* @return {number|null}
|
|
271
|
+
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views
|
|
272
|
+
*/
|
|
273
|
+
countOf(entry) {
|
|
274
|
+
const fromProp = this.counts ? this.counts[entry.index] : undefined;
|
|
275
|
+
const value = (fromProp !== undefined && fromProp !== null) ? fromProp : entry.tab.count;
|
|
276
|
+
return (typeof value === 'number' && Number.isFinite(value)) ? value : null
|
|
277
|
+
},
|
|
278
|
+
|
|
243
279
|
/**
|
|
244
280
|
* Is tab `i` rendered active in chips mode (handles both single
|
|
245
281
|
* `activeIndex` and `multiple` `selectedIndices`).
|