@conduction/nextcloud-vue 2.83.0 → 2.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +239 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +5 -5
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +50 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +226 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +126 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +106 -3
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +135 -11
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +39 -6
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/composables/useListView.js +21 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +38 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +78 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +1030 -533
- package/dist/esm/store/useObjectStore.js +16 -2
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/boardColumns.js +8 -1
- package/dist/esm/utils/boardColumns.js.map +1 -1
- package/dist/esm/utils/boardReferenceColumns.js +278 -0
- package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
- package/dist/esm/utils/buildManifest.js +41 -4
- package/dist/esm/utils/buildManifest.js.map +1 -1
- package/dist/esm/utils/detailWidgetVisibility.js +149 -0
- package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
- package/dist/esm/utils/formatMetric.js +39 -3
- package/dist/esm/utils/formatMetric.js.map +1 -1
- package/dist/esm/utils/gridStack.js +40 -1
- package/dist/esm/utils/gridStack.js.map +1 -1
- package/dist/esm/utils/humaniseKey.js +41 -0
- package/dist/esm/utils/humaniseKey.js.map +1 -0
- package/dist/esm/utils/notificationRuleLabel.js +6 -23
- package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +1 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +9 -0
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetDispatch.js +8 -2
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +17159 -13422
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1030 -533
- package/l10n/en.json +38 -1
- package/l10n/nl.json +78 -1
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
- package/src/components/CnBoardView/CnBoardView.vue +262 -6
- package/src/components/CnBodySections/CnBodySections.vue +22 -1
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +244 -8
- package/src/components/CnDashboardPage/CnDashboardPage.vue +62 -2
- package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
- package/src/components/CnDetailPage/CnDetailPage.md +3 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +226 -8
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
- package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
- package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
- package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +154 -5
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +106 -3
- package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
- package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
- package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
- package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
- package/src/components/CnStatWidget/CnStatWidget.vue +137 -12
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
- package/src/components/CnTabsWidget/CnTabsWidget.vue +39 -6
- package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
- package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
- package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
- package/src/composables/useListView.js +21 -0
- package/src/css/board.css +65 -4
- package/src/css/dialog.css +4 -2
- package/src/css/form-field.css +14 -0
- package/src/css/form-widget.css +11 -0
- package/src/css/index-page.css +49 -0
- package/src/css/kpi-card.css +13 -0
- package/src/css/look-board-reports.css +58 -0
- package/src/css/look-board.css +108 -7
- package/src/dialogs/CnTransitionInputDialog.vue +87 -7
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +137 -6
- package/src/store/useObjectStore.js +16 -2
- package/src/utils/boardColumns.js +8 -1
- package/src/utils/boardReferenceColumns.js +274 -0
- package/src/utils/buildManifest.js +40 -3
- package/src/utils/detailWidgetVisibility.js +145 -0
- package/src/utils/formatMetric.js +38 -2
- package/src/utils/gridStack.js +39 -0
- package/src/utils/humaniseKey.js +38 -0
- package/src/utils/notificationRuleLabel.js +5 -23
- package/src/utils/resolveManifestTokens.js +1 -0
- package/src/utils/sentinelTokens.js +9 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/widgetDispatch.js +8 -2
|
@@ -43,6 +43,18 @@ const VARIANT_COLORS = Object.freeze({
|
|
|
43
43
|
* negative number: "-3 days left" is a puzzle, and an overdue case is the one
|
|
44
44
|
* state a handler must not have to decode.
|
|
45
45
|
*
|
|
46
|
+
* ## A settled record
|
|
47
|
+
*
|
|
48
|
+
* `settledField` names a property on the record that is truthy once the work
|
|
49
|
+
* the date belongs to is done, such as a case in its final status. A settled
|
|
50
|
+
* record reads `settledLabel` (default "Closed") in no threshold colour, never
|
|
51
|
+
* "overdue": a closed case missed nothing that can still be acted on, and a red
|
|
52
|
+
* count of days on it sends a handler after a case that needs nobody.
|
|
53
|
+
*
|
|
54
|
+
* ```js
|
|
55
|
+
* content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
46
58
|
* ## An unset date
|
|
47
59
|
*
|
|
48
60
|
* Renders a dash and no threshold colour. A case with no deadline is not urgent
|
|
@@ -65,8 +77,10 @@ var script = {
|
|
|
65
77
|
* - `icon` — MDI icon name.
|
|
66
78
|
* - `thresholds` — `{ warn, danger }` in days remaining.
|
|
67
79
|
* - `showDate` — render the absolute date underneath (default true).
|
|
80
|
+
* - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.
|
|
81
|
+
* - `settledLabel` — the headline of a settled record, run through the host's translate function (default "Closed").
|
|
68
82
|
*
|
|
69
|
-
* @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}
|
|
83
|
+
* @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}
|
|
70
84
|
*/
|
|
71
85
|
content: {
|
|
72
86
|
type: Object,
|
|
@@ -89,6 +103,7 @@ var script = {
|
|
|
89
103
|
return {
|
|
90
104
|
cnObjectContextInjected: inject('cnObjectContext', null),
|
|
91
105
|
cnDetailObjectContextInjected: inject('cnDetailObjectContext', null),
|
|
106
|
+
cnTranslate: inject('cnTranslate', null),
|
|
92
107
|
}
|
|
93
108
|
},
|
|
94
109
|
|
|
@@ -140,17 +155,44 @@ var script = {
|
|
|
140
155
|
return Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)
|
|
141
156
|
},
|
|
142
157
|
|
|
143
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* Whether the record says its work is done (`settledField` is truthy).
|
|
160
|
+
*
|
|
161
|
+
* @return {boolean} True for a settled record.
|
|
162
|
+
* @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
|
|
163
|
+
*/
|
|
164
|
+
isSettled() {
|
|
165
|
+
const field = this.content?.settledField;
|
|
166
|
+
if (!this.record || !field) {
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
return Boolean(getByPath(this.record, field))
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Whether the date has passed on a record still open.
|
|
174
|
+
*
|
|
175
|
+
* @return {boolean} True when overdue.
|
|
176
|
+
* @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
|
|
177
|
+
*/
|
|
144
178
|
isOverdue() {
|
|
145
|
-
return this.daysRemaining !== null && this.daysRemaining < 0
|
|
179
|
+
return !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0
|
|
146
180
|
},
|
|
147
181
|
|
|
148
182
|
/**
|
|
149
183
|
* The headline: the remaining time in words.
|
|
150
184
|
*
|
|
151
185
|
* @return {string} The headline text.
|
|
186
|
+
* @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
|
|
152
187
|
*/
|
|
153
188
|
headline() {
|
|
189
|
+
if (this.isSettled) {
|
|
190
|
+
const label = this.content?.settledLabel;
|
|
191
|
+
if (label) {
|
|
192
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label
|
|
193
|
+
}
|
|
194
|
+
return translate('nextcloud-vue', 'Closed')
|
|
195
|
+
}
|
|
154
196
|
const days = this.daysRemaining;
|
|
155
197
|
if (days === null) {
|
|
156
198
|
return '—'
|
|
@@ -187,10 +229,11 @@ var script = {
|
|
|
187
229
|
* The threshold band this date falls in.
|
|
188
230
|
*
|
|
189
231
|
* @return {string} One of default / success / warning / error.
|
|
232
|
+
* @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
|
|
190
233
|
*/
|
|
191
234
|
variant() {
|
|
192
235
|
const days = this.daysRemaining;
|
|
193
|
-
if (days === null) {
|
|
236
|
+
if (days === null || this.isSettled) {
|
|
194
237
|
return 'default'
|
|
195
238
|
}
|
|
196
239
|
if (days < 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnCountdownWidget.vue2.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.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-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\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 { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/** @return {boolean} true once the date has passed. */\n\t\tisOverdue() {\n\t\t\treturn this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t */\n\t\theadline() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["CnWidgetIcon","t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnCountdownWidget.vue2.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.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-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\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 { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## A settled record\n *\n * `settledField` names a property on the record that is truthy once the work\n * the date belongs to is done, such as a case in its final status. A settled\n * record reads `settledLabel` (default \"Closed\") in no threshold colour, never\n * \"overdue\": a closed case missed nothing that can still be acted on, and a red\n * count of days on it sends a handler after a case that needs nobody.\n *\n * ```js\n * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }\n * ```\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.\n\t\t * - `settledLabel` — the headline of a settled record, run through the host's translate function (default \"Closed\").\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t\tcnTranslate: inject('cnTranslate', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/**\n\t\t * Whether the record says its work is done (`settledField` is truthy).\n\t\t *\n\t\t * @return {boolean} True for a settled record.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisSettled() {\n\t\t\tconst field = this.content?.settledField\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(getByPath(this.record, field))\n\t\t},\n\n\t\t/**\n\t\t * Whether the date has passed on a record still open.\n\t\t *\n\t\t * @return {boolean} True when overdue.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisOverdue() {\n\t\t\treturn !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\theadline() {\n\t\t\tif (this.isSettled) {\n\t\t\t\tconst label = this.content?.settledLabel\n\t\t\t\tif (label) {\n\t\t\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label\n\t\t\t\t}\n\t\t\t\treturn t('nextcloud-vue', 'Closed')\n\t\t\t}\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null || this.isSettled) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["CnWidgetIcon","t"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnCountdownWidget.vue3.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue?vue&type=template&id=17598258&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-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\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 { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/** @return {boolean} true once the date has passed. */\n\t\tisOverdue() {\n\t\t\treturn this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t */\n\t\theadline() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["_normalizeClass","_createVNode","_createElementBlock","_toDisplayString","_createElementVNode"],"mappings":";;AAgBO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,mBAAI,GAAA;;;AACT,EAAA,KAAA,EAAA,oBAAA;;;;;;;;;;;IANH,KAAM,EAAAA,cAAA,CAAA,CAAC,iCAAoB,EAAA,CAAA,qBAAA,EAAA,QAAA,CAAA,OAAA,CAAA,CAAA,CAAA,CAAA;;sBAGxB,CAAA,KAAA,EAAA;AAFA,MAAA,KAAI,EAAC,6CAA0C;AAAA,MAAA,KAAA,iBAAA,QAAA,CAAA,eAAA,CAAA;;MACnDC,WAAkB,CAAA,uBAAA,EAAA;AAAJ,QAAA,IAAI,EAAA,QAAA,CAAA,YAAA;AAAA,QAAA,IAAA,EAAA,EAAA;;;;OAId,QAAA,CAAA,aAAA;wBAALC,kBAEM,CAAA,KAAA,EAFN,UAEM,EAAAC,eAAA,CADD,QAAY,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;MAEjBC,kBAEG,CAAA,KAAA,EAAA;AAFE,QAAA,KAAA,EAAA,+CAAA;AAAA,QAAA,KAAA,0BAAA,QAAA,CAAA,UAAA,EAAA,CAAA;yBACD,QAAA,CAAA,QAAA,CAAA,EAAA,CAAA,mBAAA;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnCountdownWidget.vue3.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue?vue&type=template&id=5546e71c&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-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\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 { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## A settled record\n *\n * `settledField` names a property on the record that is truthy once the work\n * the date belongs to is done, such as a case in its final status. A settled\n * record reads `settledLabel` (default \"Closed\") in no threshold colour, never\n * \"overdue\": a closed case missed nothing that can still be acted on, and a red\n * count of days on it sends a handler after a case that needs nobody.\n *\n * ```js\n * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }\n * ```\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.\n\t\t * - `settledLabel` — the headline of a settled record, run through the host's translate function (default \"Closed\").\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t\tcnTranslate: inject('cnTranslate', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/**\n\t\t * Whether the record says its work is done (`settledField` is truthy).\n\t\t *\n\t\t * @return {boolean} True for a settled record.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisSettled() {\n\t\t\tconst field = this.content?.settledField\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(getByPath(this.record, field))\n\t\t},\n\n\t\t/**\n\t\t * Whether the date has passed on a record still open.\n\t\t *\n\t\t * @return {boolean} True when overdue.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisOverdue() {\n\t\t\treturn !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\theadline() {\n\t\t\tif (this.isSettled) {\n\t\t\t\tconst label = this.content?.settledLabel\n\t\t\t\tif (label) {\n\t\t\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label\n\t\t\t\t}\n\t\t\t\treturn t('nextcloud-vue', 'Closed')\n\t\t\t}\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null || this.isSettled) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["_normalizeClass","_createVNode","_createElementBlock","_toDisplayString","_createElementVNode"],"mappings":";;AAgBO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,mBAAI,GAAA;;;AACT,EAAA,KAAA,EAAA,oBAAA;;;;;;;;;;;IANH,KAAM,EAAAA,cAAA,CAAA,CAAC,iCAAoB,EAAA,CAAA,qBAAA,EAAA,QAAA,CAAA,OAAA,CAAA,CAAA,CAAA,CAAA;;sBAGxB,CAAA,KAAA,EAAA;AAFA,MAAA,KAAI,EAAC,6CAA0C;AAAA,MAAA,KAAA,iBAAA,QAAA,CAAA,eAAA,CAAA;;MACnDC,WAAkB,CAAA,uBAAA,EAAA;AAAJ,QAAA,IAAI,EAAA,QAAA,CAAA,YAAA;AAAA,QAAA,IAAA,EAAA,EAAA;;;;OAId,QAAA,CAAA,aAAA;wBAALC,kBAEM,CAAA,KAAA,EAFN,UAEM,EAAAC,eAAA,CADD,QAAY,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;MAEjBC,kBAEG,CAAA,KAAA,EAAA;AAFE,QAAA,KAAA,EAAA,+CAAA;AAAA,QAAA,KAAA,0BAAA,QAAA,CAAA,UAAA,EAAA,CAAA;yBACD,QAAA,CAAA,QAAA,CAAA,EAAA,CAAA,mBAAA;;;;;;;;;;"}
|
|
@@ -2,7 +2,7 @@ import script from './CnDashboardGrid.vue2.js';
|
|
|
2
2
|
import { render } from './CnDashboardGrid.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-a91998fc";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { translate } from '@nextcloud/l10n';
|
|
2
2
|
import { GridStack } from 'gridstack';
|
|
3
|
+
import { planGrowth } from '../../utils/gridStack.js';
|
|
3
4
|
|
|
4
5
|
// `gridstack` is a peerDependency (see package.json), not bundled — this
|
|
5
6
|
// component's JS and the consumer's GridStack stylesheet MUST come from the
|
|
@@ -29,6 +30,24 @@ let domIdCounter = 0;
|
|
|
29
30
|
*/
|
|
30
31
|
const MENU_ANCHOR_OFFSET = 8;
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* How many GridStack rows a layout row is drawn as on a grid with
|
|
35
|
+
* size-to-content cells (`wantsRowSplit`): 80px rows become 10px rows.
|
|
36
|
+
*/
|
|
37
|
+
const ROW_SPLIT = 8;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether a layout holds a size-to-content cell (`sizeToContent` true or a
|
|
41
|
+
* positive row maximum).
|
|
42
|
+
*
|
|
43
|
+
* @param {Array<object>} layout The layout items.
|
|
44
|
+
* @return {boolean} True when one does.
|
|
45
|
+
*/
|
|
46
|
+
function hasFitCell(layout) {
|
|
47
|
+
return Array.isArray(layout) && layout.some((item) => item
|
|
48
|
+
&& (item.sizeToContent === true || (Number.isFinite(item.sizeToContent) && item.sizeToContent > 0)))
|
|
49
|
+
}
|
|
50
|
+
|
|
32
51
|
/**
|
|
33
52
|
* CnDashboardGrid — Low-level grid layout engine powered by GridStack.
|
|
34
53
|
*
|
|
@@ -234,10 +253,62 @@ var script = {
|
|
|
234
253
|
* on that.
|
|
235
254
|
*/
|
|
236
255
|
reflowed: false,
|
|
256
|
+
/**
|
|
257
|
+
* How many GridStack rows one layout row is drawn as: 1, or
|
|
258
|
+
* ROW_SPLIT while `wantsRowSplit` holds. Set before the first render,
|
|
259
|
+
* since GridStack reads the items' places from their attributes,
|
|
260
|
+
* and again when the grid is rebuilt.
|
|
261
|
+
*/
|
|
262
|
+
rowSplit: (!this.editable && !this.float && hasFitCell(this.layout)) ? ROW_SPLIT : 1,
|
|
237
263
|
}
|
|
238
264
|
},
|
|
239
265
|
|
|
240
266
|
computed: {
|
|
267
|
+
/**
|
|
268
|
+
* The layout's items top to bottom, then left to right: the order the
|
|
269
|
+
* items are in the page, so GridStack places them in that order.
|
|
270
|
+
*
|
|
271
|
+
* GridStack sizes a size-to-content cell while it adds the items, one
|
|
272
|
+
* at a time, and an item it adds onto a cell that has already grown
|
|
273
|
+
* moves itself below whatever it then collides with. In the layout's
|
|
274
|
+
* own order a card declared after the lists (pipelinq's full-width
|
|
275
|
+
* "client in focus" card) was added last, so when the banner above
|
|
276
|
+
* it grew it skipped past the stat tiles AND the lists and landed at
|
|
277
|
+
* the bottom. Added top to bottom, every card lands under the one
|
|
278
|
+
* above it. It is also the order a screen reader and the Tab key
|
|
279
|
+
* follow. An item without a row or column sorts after the items
|
|
280
|
+
* that have one, in the layout's order.
|
|
281
|
+
*
|
|
282
|
+
* @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it
|
|
283
|
+
* @return {Array<object>} The items in row order.
|
|
284
|
+
*/
|
|
285
|
+
itemsInRowOrder() {
|
|
286
|
+
const items = Array.isArray(this.layout) ? [...this.layout] : [];
|
|
287
|
+
const at = (value) => (Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER);
|
|
288
|
+
return items.sort((a, b) => (at(a.gridY) - at(b.gridY)) || (at(a.gridX) - at(b.gridX)))
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Whether the grid draws each layout row as ROW_SPLIT finer rows. A
|
|
293
|
+
* size-to-content cell can only be a whole number of rows tall, so on
|
|
294
|
+
* 80px rows it kept up to a row of empty space under its content and
|
|
295
|
+
* a packing grid (`float: false`) could not close it (pipelinq and
|
|
296
|
+
* dossiq Mijn werk, decidiq Mijn acties). Split into 10px rows the
|
|
297
|
+
* cell ends within 10px of its content and the widgets below move up.
|
|
298
|
+
*
|
|
299
|
+
* Only in view mode, only on a packing grid with a size-to-content
|
|
300
|
+
* cell: every other grid is built exactly as before. Item positions
|
|
301
|
+
* and sizes scale with the rows, so every widget keeps its pixel
|
|
302
|
+
* place and size until a cell shrinks; the layout itself, what edit
|
|
303
|
+
* mode shows and what `layout-change` reports, stays in layout rows.
|
|
304
|
+
*
|
|
305
|
+
* @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content
|
|
306
|
+
* @return {boolean} True to split the rows.
|
|
307
|
+
*/
|
|
308
|
+
wantsRowSplit() {
|
|
309
|
+
return !this.editable && !this.float && hasFitCell(this.layout)
|
|
310
|
+
},
|
|
311
|
+
|
|
241
312
|
/**
|
|
242
313
|
* @return {boolean} Whether the keyboard lane is live — items are
|
|
243
314
|
* focusable and the arrow keys reposition. Requires the opt-in prop,
|
|
@@ -304,6 +375,12 @@ var script = {
|
|
|
304
375
|
this.syncInteractivity();
|
|
305
376
|
},
|
|
306
377
|
|
|
378
|
+
wantsRowSplit(wants) {
|
|
379
|
+
if (this.grid && (wants ? ROW_SPLIT : 1) !== this.rowSplit) {
|
|
380
|
+
this.rebuildGrid();
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
|
|
307
384
|
layout: {
|
|
308
385
|
deep: true,
|
|
309
386
|
handler(newLayout) {
|
|
@@ -363,11 +440,75 @@ var script = {
|
|
|
363
440
|
}
|
|
364
441
|
// A number is GridStack's "size to content, but never taller than N rows" (a maximum).
|
|
365
442
|
if (Number.isFinite(v) && v > 0) {
|
|
366
|
-
return String(v)
|
|
443
|
+
return String(v * this.rowSplit)
|
|
367
444
|
}
|
|
368
445
|
return undefined
|
|
369
446
|
},
|
|
370
447
|
|
|
448
|
+
/**
|
|
449
|
+
* A layout row coordinate or height in GridStack rows (`rowSplit`).
|
|
450
|
+
*
|
|
451
|
+
* @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content
|
|
452
|
+
* @param {number|undefined} value The value in layout rows.
|
|
453
|
+
* @return {number|undefined} The value in grid rows, or undefined when none is set.
|
|
454
|
+
*/
|
|
455
|
+
scaledRow(value) {
|
|
456
|
+
return Number.isFinite(value) ? value * this.rowSplit : undefined
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* The smallest height an item may take, in grid rows: `minHeight`
|
|
461
|
+
* layout rows, except for a size-to-content cell on split rows, which
|
|
462
|
+
* may shrink to its content down to one grid row. The minimum is for
|
|
463
|
+
* resizing by hand, and that only happens on whole rows in edit mode.
|
|
464
|
+
*
|
|
465
|
+
* @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content
|
|
466
|
+
* @param {object} item The layout item.
|
|
467
|
+
* @return {number} The minimum height in grid rows.
|
|
468
|
+
*/
|
|
469
|
+
minRows(item) {
|
|
470
|
+
if (this.rowSplit !== 1 && this.sizeToContentAttr(item) !== undefined) {
|
|
471
|
+
return 1
|
|
472
|
+
}
|
|
473
|
+
return this.minHeight * this.rowSplit
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Build the grid again with the row split `wantsRowSplit` asks for, as
|
|
478
|
+
* edit mode turns on or off: the items go back to their layout places in
|
|
479
|
+
* the new rows, and the size-to-content cells are measured again.
|
|
480
|
+
*
|
|
481
|
+
* @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content
|
|
482
|
+
* @return {void}
|
|
483
|
+
*/
|
|
484
|
+
rebuildGrid() {
|
|
485
|
+
this.disconnectContentObserver();
|
|
486
|
+
this.grid.destroy(false);
|
|
487
|
+
this.grid = null;
|
|
488
|
+
this.rowSplit = this.wantsRowSplit ? ROW_SPLIT : 1;
|
|
489
|
+
this.$nextTick(() => {
|
|
490
|
+
if (!this.$refs.gridContainer) {
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
// GridStack wrote its own places onto the items; put the layout's back.
|
|
494
|
+
for (const item of this.layout) {
|
|
495
|
+
const el = this.$refs.gridContainer.querySelector(`[gs-id="${item.id}"]`);
|
|
496
|
+
if (!el) {
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
499
|
+
for (const [name, value] of [['gs-y', this.scaledRow(item.gridY)], ['gs-h', this.scaledRow(item.gridHeight)], ['gs-min-h', this.minRows(item)], ['gs-size-to-content', this.sizeToContentAttr(item)]]) {
|
|
500
|
+
if (value === undefined) {
|
|
501
|
+
el.removeAttribute(name);
|
|
502
|
+
} else {
|
|
503
|
+
el.setAttribute(name, String(value));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
this.initGrid();
|
|
508
|
+
this.observeContentSizes();
|
|
509
|
+
});
|
|
510
|
+
},
|
|
511
|
+
|
|
371
512
|
/**
|
|
372
513
|
* Build the accessible name for a grid item. A custom `itemLabel` wins
|
|
373
514
|
* outright; otherwise the first human-readable field on the item is used
|
|
@@ -422,6 +563,13 @@ var script = {
|
|
|
422
563
|
)
|
|
423
564
|
},
|
|
424
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Build the GridStack grid on the rendered items, install the growth
|
|
568
|
+
* wrapper (`keepOrderOnGrowth`) and listen for its changes.
|
|
569
|
+
*
|
|
570
|
+
* @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it
|
|
571
|
+
* @return {void}
|
|
572
|
+
*/
|
|
425
573
|
initGrid() {
|
|
426
574
|
if (this.cellHeightCssVar && typeof document !== 'undefined' && document.documentElement) {
|
|
427
575
|
document.documentElement.style.setProperty(this.cellHeightCssVar, `${this.cellHeight}px`);
|
|
@@ -429,7 +577,7 @@ var script = {
|
|
|
429
577
|
const el = this.$refs.gridContainer.querySelector('.grid-stack');
|
|
430
578
|
this.grid = GridStack.init({
|
|
431
579
|
column: this.columns,
|
|
432
|
-
cellHeight: this.cellHeight,
|
|
580
|
+
cellHeight: this.cellHeight / this.rowSplit,
|
|
433
581
|
margin: this.margin,
|
|
434
582
|
float: this.float,
|
|
435
583
|
animate: true,
|
|
@@ -438,6 +586,7 @@ var script = {
|
|
|
438
586
|
removable: false,
|
|
439
587
|
...(this.columnOpts ? { columnOpts: this.columnOpts } : {}),
|
|
440
588
|
}, el);
|
|
589
|
+
this.keepOrderOnGrowth();
|
|
441
590
|
|
|
442
591
|
this.grid.on('change', (_event, items) => {
|
|
443
592
|
this.handleGridChange(items);
|
|
@@ -449,6 +598,87 @@ var script = {
|
|
|
449
598
|
this.syncInteractivity();
|
|
450
599
|
},
|
|
451
600
|
|
|
601
|
+
/**
|
|
602
|
+
* Let a size-to-content cell that grows push the cards under it down in
|
|
603
|
+
* their order. GridStack resolves that collision itself, but not in
|
|
604
|
+
* order: when pipelinq's banner grew from two rows to three, the
|
|
605
|
+
* full-width card under the stat tiles landed below the lists, also
|
|
606
|
+
* with `float: false`. Every grow after the grid is built, whether
|
|
607
|
+
* GridStack measures it on resize or this grid's content observer asks
|
|
608
|
+
* for it, now first moves the cards the growth reaches down by the
|
|
609
|
+
* growth (`planGrowth`), bottom-most first, and only then grows the
|
|
610
|
+
* cell, so GridStack has no collision left to resolve. The wrapper is
|
|
611
|
+
* installed after `GridStack.init` has already measured the cells on
|
|
612
|
+
* load; the order on load comes from adding the items in row order
|
|
613
|
+
* (`itemsInRowOrder`). A cell that shrinks is left to GridStack as
|
|
614
|
+
* before.
|
|
615
|
+
*
|
|
616
|
+
* @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it
|
|
617
|
+
* @return {void}
|
|
618
|
+
*/
|
|
619
|
+
keepOrderOnGrowth() {
|
|
620
|
+
const grid = this.grid;
|
|
621
|
+
if (!grid || typeof grid.resizeToContent !== 'function' || typeof grid.update !== 'function') {
|
|
622
|
+
return
|
|
623
|
+
}
|
|
624
|
+
const resize = grid.resizeToContent.bind(grid);
|
|
625
|
+
grid.resizeToContent = (el) => {
|
|
626
|
+
const node = el && el.gridstackNode;
|
|
627
|
+
const nodes = grid.engine && Array.isArray(grid.engine.nodes) ? grid.engine.nodes : null;
|
|
628
|
+
const rows = node && nodes ? this.contentRows(el, node) : null;
|
|
629
|
+
const plan = rows ? planGrowth(nodes, node, rows) : [];
|
|
630
|
+
if (plan.length === 0) {
|
|
631
|
+
resize(el);
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
// Inside a batch the moves are not packed until the cell has grown.
|
|
635
|
+
const batching = Boolean(grid.engine.batchMode);
|
|
636
|
+
if (!batching) {
|
|
637
|
+
grid.batchUpdate();
|
|
638
|
+
}
|
|
639
|
+
for (const step of plan) {
|
|
640
|
+
grid.update(step.node.el, { y: step.y });
|
|
641
|
+
}
|
|
642
|
+
resize(el);
|
|
643
|
+
if (!batching) {
|
|
644
|
+
grid.batchUpdate(false);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
},
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* The rows a size-to-content cell needs for its content, measured the
|
|
651
|
+
* way GridStack's `resizeToContent` measures it (the content's first
|
|
652
|
+
* child, plus the cell's own padding), within its size-to-content row
|
|
653
|
+
* maximum and its minimum and maximum height.
|
|
654
|
+
*
|
|
655
|
+
* @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it
|
|
656
|
+
* @param {HTMLElement} el The cell.
|
|
657
|
+
* @param {object} node Its GridStack node.
|
|
658
|
+
* @return {number|null} The rows, or null when it cannot be measured.
|
|
659
|
+
*/
|
|
660
|
+
contentRows(el, node) {
|
|
661
|
+
const cell = this.grid.getCellHeight(true);
|
|
662
|
+
const item = el.querySelector('.grid-stack-item-content');
|
|
663
|
+
const child = item && item.firstElementChild;
|
|
664
|
+
if (!el.clientHeight || !cell || !child || !Number.isFinite(node.h)) {
|
|
665
|
+
return null
|
|
666
|
+
}
|
|
667
|
+
const padding = el.clientHeight - item.clientHeight;
|
|
668
|
+
const itemHeight = node.h * cell - padding;
|
|
669
|
+
const wanted = child.getBoundingClientRect().height || itemHeight;
|
|
670
|
+
let rows = Math.ceil((node.h * cell + wanted - itemHeight) / cell);
|
|
671
|
+
if (Number.isInteger(node.sizeToContent) && rows > node.sizeToContent) {
|
|
672
|
+
rows = node.sizeToContent;
|
|
673
|
+
}
|
|
674
|
+
if (node.minH && rows < node.minH) {
|
|
675
|
+
rows = node.minH;
|
|
676
|
+
} else if (node.maxH && rows > node.maxH) {
|
|
677
|
+
rows = node.maxH;
|
|
678
|
+
}
|
|
679
|
+
return rows
|
|
680
|
+
},
|
|
681
|
+
|
|
452
682
|
/**
|
|
453
683
|
* Keep both editing lanes in step with whether an edit could actually be
|
|
454
684
|
* kept: the pointer's handles, and — through `reflowed` feeding
|
|
@@ -516,6 +746,12 @@ var script = {
|
|
|
516
746
|
if (this.isReflowed()) {
|
|
517
747
|
return
|
|
518
748
|
}
|
|
749
|
+
// A grid on split rows is a view-mode grid: what changes in it is
|
|
750
|
+
// packing and size-to-content measuring, never a user's
|
|
751
|
+
// arrangement, and it is not reported as one.
|
|
752
|
+
if (this.rowSplit !== 1) {
|
|
753
|
+
return
|
|
754
|
+
}
|
|
519
755
|
if (!items || items.length === 0) {
|
|
520
756
|
return
|
|
521
757
|
}
|
|
@@ -916,7 +1152,7 @@ var script = {
|
|
|
916
1152
|
// grid's own position back, which matches and does
|
|
917
1153
|
// nothing here.
|
|
918
1154
|
const x = item.gridX ?? node.x;
|
|
919
|
-
const y = item.gridY
|
|
1155
|
+
const y = Number.isFinite(item.gridY) ? item.gridY * this.rowSplit : node.y;
|
|
920
1156
|
if (x !== node.x || y !== node.y) {
|
|
921
1157
|
// The layout said where it goes; GridStack's `change`
|
|
922
1158
|
// for it is not a user's arrangement and is not
|