@conduction/nextcloud-vue 2.77.0 → 2.79.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.
Files changed (159) hide show
  1. package/dist/bundled-packages.json +0 -2
  2. package/dist/esm/bundled-packages.json +0 -2
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +113 -104
  5. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js +23 -0
  7. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  12. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +2 -1
  13. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  14. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  15. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +117 -0
  16. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  17. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +122 -97
  18. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  19. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +37 -0
  20. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  21. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +2 -2
  22. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +168 -5
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +84 -2
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +4 -2
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue.js +1 -1
  32. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js +45 -2
  33. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js.map +1 -1
  34. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue3.js.map +1 -1
  35. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +61 -1
  36. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  38. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
  39. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  40. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
  41. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  42. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
  43. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  44. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
  45. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
  46. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +8 -2
  47. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  49. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +4 -1
  50. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  51. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +31 -2
  53. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +3 -2
  55. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  56. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
  57. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  58. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
  59. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  60. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
  61. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  62. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  63. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  64. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
  65. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  66. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +36 -0
  67. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  68. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +35 -23
  69. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  70. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js +6 -2
  71. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js.map +1 -1
  72. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue3.js.map +1 -1
  73. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +30 -1
  74. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  76. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  77. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  78. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  79. package/dist/esm/composables/useAppManifest.js +8 -3
  80. package/dist/esm/composables/useAppManifest.js.map +1 -1
  81. package/dist/esm/composables/useFormDraft.js +11 -1
  82. package/dist/esm/composables/useFormDraft.js.map +1 -1
  83. package/dist/esm/composables/useObjectPresence.js +15 -4
  84. package/dist/esm/composables/useObjectPresence.js.map +1 -1
  85. package/dist/esm/index.js +1 -0
  86. package/dist/esm/index.js.map +1 -1
  87. package/dist/esm/l10n/en.json.js +68 -1
  88. package/dist/esm/l10n/en.json.js.map +1 -1
  89. package/dist/esm/l10n/nl.json.js +113 -2
  90. package/dist/esm/l10n/nl.json.js.map +1 -1
  91. package/dist/esm/nextcloud-vue.css +526 -16
  92. package/dist/esm/utils/badgeVariants.js +32 -0
  93. package/dist/esm/utils/badgeVariants.js.map +1 -0
  94. package/dist/esm/utils/boardDate.js +193 -0
  95. package/dist/esm/utils/boardDate.js.map +1 -0
  96. package/dist/esm/utils/dynamicProperties.js +5 -1
  97. package/dist/esm/utils/dynamicProperties.js.map +1 -1
  98. package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
  99. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  100. package/dist/nextcloud-vue.cjs.js +2569 -811
  101. package/dist/nextcloud-vue.cjs.js.map +2 -2
  102. package/dist/nextcloud-vue.css +526 -16
  103. package/l10n/en.json +68 -1
  104. package/l10n/nl.json +113 -2
  105. package/package.json +1 -1
  106. package/scripts/build-validators.js +26 -0
  107. package/src/components/CnActionsBar/CnActionsBar.vue +48 -39
  108. package/src/components/CnAiCompanion/CnAiCompanion.vue +23 -0
  109. package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
  110. package/src/components/CnBoardView/CnBoardView.vue +2 -1
  111. package/src/components/CnCellRenderer/CnCellRenderer.md +1 -0
  112. package/src/components/CnCellRenderer/CnCellRenderer.vue +169 -10
  113. package/src/components/CnChartWidget/CnChartWidget.vue +39 -2
  114. package/src/components/CnDashboardPage/CnDashboardPage.vue +248 -8
  115. package/src/components/CnDataTable/CnDataTable.md +2 -0
  116. package/src/components/CnDataTable/CnDataTable.vue +88 -4
  117. package/src/components/CnDateRangePicker/CnDateRangePicker.vue +44 -1
  118. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +61 -1
  119. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
  120. package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
  121. package/src/components/CnFilesBrowser/index.js +1 -0
  122. package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
  123. package/src/components/CnFormDialog/CnFormDialog.vue +8 -2
  124. package/src/components/CnFormPage/CnFormPage.vue +4 -1
  125. package/src/components/CnIndexPage/CnIndexPage.vue +33 -3
  126. package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
  127. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
  128. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
  129. package/src/components/CnStatWidget/CnStatWidget.vue +52 -2
  130. package/src/components/CnStatusBadge/CnStatusBadge.vue +6 -2
  131. package/src/components/CnTabsWidget/CnTabsWidget.vue +30 -1
  132. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  133. package/src/components/index.js +1 -1
  134. package/src/composables/__tests__/useObjectPresence.spec.js +24 -0
  135. package/src/composables/useAppManifest.js +8 -3
  136. package/src/composables/useFormDraft.js +11 -1
  137. package/src/composables/useObjectPresence.js +15 -4
  138. package/src/css/badge.css +31 -0
  139. package/src/css/kpi-card.css +51 -0
  140. package/src/css/look-board-detail.css +10 -0
  141. package/src/css/look-board-index.css +128 -14
  142. package/src/css/look-board.css +85 -0
  143. package/src/css/table.css +37 -0
  144. package/src/index.js +4 -0
  145. package/src/schemas/app-manifest-v2.schema.json +50 -5
  146. package/src/utils/badgeVariants.js +29 -0
  147. package/src/utils/boardDate.js +189 -0
  148. package/src/utils/dynamicProperties.js +5 -1
  149. package/src/utils/validateManifestV2.compiled.js +57 -2
  150. package/dist/esm/_virtual/equal.js +0 -4
  151. package/dist/esm/_virtual/equal.js.map +0 -1
  152. package/dist/esm/_virtual/ucs2length.js +0 -4
  153. package/dist/esm/_virtual/ucs2length.js.map +0 -1
  154. package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
  155. package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
  156. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
  157. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
  158. package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
  159. package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
package/src/css/table.css CHANGED
@@ -230,6 +230,34 @@
230
230
  min-width: 0;
231
231
  }
232
232
 
233
+ /* Fit to the container (CnDataTable's `fitWidth`, on in CnWidgetObjectTable).
234
+
235
+ In a narrow tile the auto layout made the table as wide as its one-line
236
+ cells, so it ran past the tile and clipped the trailing column (a date).
237
+ Here every column but one takes the width of its content (`width: 1%` asks
238
+ for as little as the content allows), and the grow column takes what is
239
+ left. `max-width: 0` is what lets that column go below its content's width:
240
+ the table no longer counts its text, so the text is cut with an ellipsis
241
+ instead of widening the table. The full value stays in the cell's tooltip. */
242
+ .cn-data-table--fit {
243
+ width: 100%;
244
+ }
245
+
246
+ .cn-data-table--fit th,
247
+ .cn-data-table--fit td {
248
+ width: 1%;
249
+ }
250
+
251
+ .cn-data-table--fit th.cn-table-col--grow,
252
+ .cn-data-table--fit td.cn-table-col--grow {
253
+ width: auto;
254
+ min-width: 0;
255
+ max-width: 0;
256
+ overflow: hidden;
257
+ text-overflow: ellipsis;
258
+ white-space: nowrap;
259
+ }
260
+
233
261
  .cn-data-table th {
234
262
  background: var(--color-background-dark);
235
263
  font-weight: 500;
@@ -578,6 +606,15 @@
578
606
  color: var(--color-text-maxcontrast);
579
607
  }
580
608
 
609
+ /* The empty row holds a sentence (the empty text, or why a lens cannot
610
+ answer), not a cell value: it wraps instead of being cut to one line. The
611
+ selector carries the table class so it outranks the one-line cell rule. */
612
+ .cn-data-table .cn-table-empty td {
613
+ max-width: none;
614
+ white-space: normal;
615
+ overflow-wrap: anywhere;
616
+ }
617
+
581
618
  /* ========================================
582
619
  Backwards compatibility aliases
583
620
  (legacy OpenRegister CSS class names)
package/src/index.js CHANGED
@@ -338,6 +338,10 @@ export {
338
338
  // @deprecated Use `CnBuildiqEditButton`.
339
339
  export { CnOpenBuildEditButton } from './components/index.js'
340
340
 
341
+ // A CnFilesBrowser data source that reads an OpenRegister object's folder
342
+ // through OpenRegister's files API (openspec: files-browser-openregister-source).
343
+ export { createOpenRegisterSource } from './components/index.js'
344
+
341
345
  // Workplace dashboard primitives (openspec: workplace-dashboard-primitives):
342
346
  // the week strip and stacked bar widgets with their forms, the segmented
343
347
  // control, and the brand stripe.
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.75.0",
6
+ "version": "2.79.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -1094,7 +1094,7 @@
1094
1094
  "pillColors": {
1095
1095
  "type": "object",
1096
1096
  "description": "Pill value to badge variant. A value without an entry takes the default variant.",
1097
- "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info"] }
1097
+ "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] }
1098
1098
  },
1099
1099
  "due": { "type": "string", "description": "The date field of the footer's due text, marked by `dueRule`." },
1100
1100
  "owner": { "type": "string", "description": "The field of the owner shown as an avatar (initials) at the footer's end." }
@@ -1143,7 +1143,7 @@
1143
1143
  "colorMap": {
1144
1144
  "type": "object",
1145
1145
  "description": "Raw value to badge variant. Keys are matched ignoring case.",
1146
- "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info"] }
1146
+ "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] }
1147
1147
  },
1148
1148
  "labels": {
1149
1149
  "type": "object",
@@ -1152,7 +1152,7 @@
1152
1152
  },
1153
1153
  "variant": {
1154
1154
  "type": "string",
1155
- "enum": ["default", "primary", "success", "warning", "error", "info"],
1155
+ "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"],
1156
1156
  "default": "default",
1157
1157
  "description": "Badge variant for a value `colorMap` does not name."
1158
1158
  }
@@ -3586,7 +3586,7 @@
3586
3586
  },
3587
3587
  "columns": {
3588
3588
  "type": "array",
3589
- "description": "Table columns (for type='index' and type='logs'). Items may be strings (legacy shorthand) or column objects ({ key, label, sortable?, width?, formatter?, formatterOptions?, widget?, widgetProps?, format?, aggregate? }). `formatter` resolves against cnFormatters (built-ins incl. currency / conditionalPhrase — configured via `formatterOptions`); `widget` resolves against cnCellWidgets (built-ins incl. badge / fkResolve / link / avatar / date).",
3589
+ "description": "Table columns (for type='index' and type='logs'). Items may be strings (legacy shorthand) or column objects ({ key, label, sortable?, width?, formatter?, formatterOptions?, widget?, widgetProps?, format?, aggregate?, colorMap? }). `colorMap` maps an enum column's raw values to a pill tone (default, primary, success, warning, error, info, purple, teal) and wins over the schema property's own `colorMap` / `x-color-map`. `formatter` resolves against cnFormatters (built-ins incl. currency / conditionalPhrase — configured via `formatterOptions`); `widget` resolves against cnCellWidgets (built-ins incl. badge / fkResolve / link / avatar / date).",
3590
3590
  "items": {
3591
3591
  "oneOf": [
3592
3592
  { "type": "string" },
@@ -4004,6 +4004,40 @@
4004
4004
  "default": [],
4005
4005
  "description": "type='dashboard': widget ids drawn as a KPI row above the grid, in the listed order, in an auto-fit grid (`repeat(auto-fit, minmax(200px, 1fr))`, gap 16). The named widgets are not GridStack items, so they do not reflow with the grid and are not part of the emitted layout; an id that names no widget is skipped with a development warning (screens-dashboard-parity). Maps to CnDashboardPage `kpiRow`. `dateRange.control` also accepts \"segmented\": the presets as one compact segmented group under the header."
4006
4006
  },
4007
+ "greeting": {
4008
+ "oneOf": [
4009
+ { "type": "boolean" },
4010
+ { "type": "string", "enum": ["first", "full"] }
4011
+ ],
4012
+ "default": false,
4013
+ "description": "type='dashboard': greet the reader in the header subtitle. `true` or \"first\" writes \"Good afternoon, Pieter · Monday 5 October 2026\", \"full\" uses the whole display name; the page `description` follows after another middle dot (screens-dashboard-greeting-header). Maps to CnDashboardPage `greeting`."
4014
+ },
4015
+ "viewLinks": {
4016
+ "type": "array",
4017
+ "default": [],
4018
+ "description": "type='dashboard': links drawn as small pills on the switch row under the header, right of the view switch (screens-dashboard-greeting-header). `route` is a route name or a `{ name, params, query }` location, `href` a plain URL; `label` is an i18n key. An entry without a label or a target is skipped. Maps to CnDashboardPage `viewLinks`.",
4019
+ "items": {
4020
+ "type": "object",
4021
+ "required": ["label"],
4022
+ "additionalProperties": false,
4023
+ "properties": {
4024
+ "label": { "type": "string", "minLength": 1 },
4025
+ "icon": { "type": "string", "minLength": 1 },
4026
+ "route": {
4027
+ "oneOf": [
4028
+ { "type": "string", "minLength": 1 },
4029
+ { "type": "object", "required": ["name"], "properties": { "name": { "type": "string", "minLength": 1 }, "params": { "type": "object" }, "query": { "type": "object" } } }
4030
+ ]
4031
+ },
4032
+ "href": { "type": "string", "minLength": 1 }
4033
+ }
4034
+ }
4035
+ },
4036
+ "showActionsMenu": {
4037
+ "type": "boolean",
4038
+ "default": true,
4039
+ "description": "type='dashboard': `false` removes the page Actions menu from the header, for a board page whose header carries only the buildiq square and the primary (screens-dashboard-greeting-header). Maps to CnDashboardPage `showActionsMenu`."
4040
+ },
4007
4041
  "gridFloat": {
4008
4042
  "type": "boolean",
4009
4043
  "default": true,
@@ -4038,6 +4072,11 @@
4038
4072
  "minLength": 1,
4039
4073
  "description": "type='index', board look: free text after the footer's count, e.g. \"click a column header to sort\" (screens-index-list-parity). Maps to CnIndexPage `footerNote`."
4040
4074
  },
4075
+ "searchPlaceholder": {
4076
+ "type": "string",
4077
+ "minLength": 1,
4078
+ "description": "type='index': the placeholder (and accessible name) of the list's search field, e.g. \"Search by case, number or requester\". Run through the app's translate, so an English key with a Dutch catalogue entry reads Dutch. Absent: the library's \"Search…\" (screens-index-toolbar-parity). Maps to CnIndexPage `searchPlaceholder`."
4079
+ },
4041
4080
  "bulkHint": {
4042
4081
  "type": "string",
4043
4082
  "minLength": 1,
@@ -4065,6 +4104,12 @@
4065
4104
  }
4066
4105
  }
4067
4106
  },
4107
+ "rowTitle": {
4108
+ "type": "string",
4109
+ "enum": ["link", "plain"],
4110
+ "default": "link",
4111
+ "description": "type='index' (screens-table-rows-parity): how the board look draws the table's title column. `link` (the default) is the 15px weight 600 underlined title of the DqZaken board; `plain` is the 15px bold title without an underline over a 13px muted secondary line, as the PqTickets and OcPublicaties boards draw it. The row stays clickable either way. The Nextcloud look ignores it. Maps to CnIndexPage `rowTitle`."
4112
+ },
4068
4113
  "showTypeEyebrow": {
4069
4114
  "type": "boolean",
4070
4115
  "default": true,
@@ -0,0 +1,29 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * badgeVariants: the colour variants a CnStatusBadge (a status pill) may take.
6
+ *
7
+ * The first six follow the Nextcloud colour roles. `purple` and `teal` are the
8
+ * two tones the drawn boards use for a status that has no Nextcloud role, such
9
+ * as "In behandeling" (work in progress) and "Omgezet naar een zaak" (handed
10
+ * on). One list, so the badge, the board card pill and the manifest schema
11
+ * accept the same names.
12
+ *
13
+ * @module utils/badgeVariants
14
+ * @spec openspec/changes/screens-cell-pill-parity/specs/cell-pill-tones/spec.md#requirement-a-status-pill-has-eight-tones
15
+ */
16
+
17
+ /** Every variant a status pill accepts, in the order the docs list them. */
18
+ export const BADGE_VARIANTS = Object.freeze(['default', 'primary', 'success', 'warning', 'error', 'info', 'purple', 'teal'])
19
+
20
+ /**
21
+ * Whether a value names a badge variant.
22
+ *
23
+ * @param {unknown} variant The candidate variant.
24
+ * @return {boolean} True for one of `BADGE_VARIANTS`.
25
+ * @spec openspec/changes/screens-cell-pill-parity/specs/cell-pill-tones/spec.md#requirement-a-status-pill-has-eight-tones
26
+ */
27
+ export function isBadgeVariant(variant) {
28
+ return typeof variant === 'string' && BADGE_VARIANTS.includes(variant)
29
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * boardDate: the two short time readings the drawn boards put in a table cell.
6
+ *
7
+ * - A date ("5 okt", "30 okt", "14 feb 2024"): day and short month in the
8
+ * user's language, the year only when it is not the current year.
9
+ * - An age ("4 uur", "1 dag", "3 dagen"): how long something has been
10
+ * waiting, in hours under a day and in whole calendar days after that.
11
+ *
12
+ * Both read the Nextcloud user LANGUAGE for the month and unit names. A
13
+ * Nextcloud account keeps its locale (date order) apart from its language,
14
+ * and a Dutch reader whose locale was left at the en_US default saw
15
+ * "Feb 14, 2024": the language decides the words, so it decides here, and the
16
+ * locale is only used when it is a region of that same language.
17
+ *
18
+ * @module utils/boardDate
19
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md
20
+ */
21
+
22
+ import { getCanonicalLocale, getLanguage, translatePlural as n } from '@nextcloud/l10n'
23
+ import { parseDateValue } from './dateVariant.js'
24
+ import { compareVisibleWhen } from './visibleWhen.js'
25
+
26
+ const MS_PER_HOUR = 3600000
27
+ const MS_PER_DAY = 86400000
28
+
29
+ /** Variants an age rule may name. `danger` is accepted as an alias of `error`. */
30
+ export const AGE_VARIANTS = Object.freeze(['default', 'success', 'warning', 'error'])
31
+
32
+ /**
33
+ * Read a language or locale tag from an @nextcloud/l10n getter, which throws
34
+ * when Nextcloud has not set one (outside a Nextcloud page, in a test).
35
+ *
36
+ * @param {() => string} getter `getLanguage` or `getCanonicalLocale`.
37
+ * @return {string} The tag with `-` separators, or ''.
38
+ */
39
+ function readTag(getter) {
40
+ try {
41
+ return String(getter() || '').replace(/_/g, '-')
42
+ } catch {
43
+ return ''
44
+ }
45
+ }
46
+
47
+ /**
48
+ * The BCP 47 tag the board dates are written in: the user's locale when it is
49
+ * a region of the user's language (`nl-BE` for language `nl`), else the
50
+ * language itself.
51
+ *
52
+ * @return {string|undefined} The tag, or undefined to let Intl choose.
53
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language
54
+ */
55
+ export function boardDateLocale() {
56
+ const language = readTag(getLanguage)
57
+ const locale = readTag(getCanonicalLocale)
58
+ const base = (tag) => tag.split('-')[0].toLowerCase()
59
+ if (language && locale && base(language) === base(locale)) {
60
+ return locale
61
+ }
62
+ return language || locale || undefined
63
+ }
64
+
65
+ /**
66
+ * A date in the board's short form: "5 okt", or "14 feb 2024" outside the
67
+ * current year.
68
+ *
69
+ * @param {unknown} value A Date, a timestamp or an ISO string.
70
+ * @param {Date} [now] The reference moment (defaults to the current time).
71
+ * @param {string} [locale] BCP 47 tag (defaults to `boardDateLocale()`).
72
+ * @return {string} The short date, or '' when the value is not a date.
73
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language
74
+ */
75
+ export function formatBoardDate(value, now = new Date(), locale = boardDateLocale()) {
76
+ const date = parseDateValue(value)
77
+ if (date === null) {
78
+ return ''
79
+ }
80
+ const options = { day: 'numeric', month: 'short' }
81
+ if (date.getFullYear() !== now.getFullYear()) {
82
+ options.year = 'numeric'
83
+ }
84
+ try {
85
+ return new Intl.DateTimeFormat(locale, options).format(date)
86
+ } catch {
87
+ return new Intl.DateTimeFormat(undefined, options).format(date)
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Whole calendar days from the date until `now` (0 today, 1 yesterday), or
93
+ * null when the value is not a date. A future date gives a negative number.
94
+ *
95
+ * @param {unknown} value The date.
96
+ * @param {Date} [now] The reference moment.
97
+ * @return {number|null} The day count.
98
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-says-how-long-something-has-waited
99
+ */
100
+ export function ageInDays(value, now = new Date()) {
101
+ const date = parseDateValue(value)
102
+ if (date === null) {
103
+ return null
104
+ }
105
+ // UTC midnights of the two LOCAL days, so a daylight-saving hour never
106
+ // turns one day into zero or two.
107
+ const start = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
108
+ const today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())
109
+ return Math.round((today - start) / MS_PER_DAY)
110
+ }
111
+
112
+ /**
113
+ * How long something has waited: "4 uur" under 24 hours (at least 1), else
114
+ * "1 dag" / "3 dagen" in whole calendar days. Plural forms come from the
115
+ * library's own catalogue (`nextcloud-vue`). A date in the future reads as
116
+ * waiting no time: "0 hours".
117
+ *
118
+ * @param {unknown} value The moment the wait started.
119
+ * @param {Date} [now] The reference moment.
120
+ * @return {string} The age, or '' when the value is not a date.
121
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-says-how-long-something-has-waited
122
+ */
123
+ export function formatAge(value, now = new Date()) {
124
+ const date = parseDateValue(value)
125
+ if (date === null) {
126
+ return ''
127
+ }
128
+ const elapsed = now.getTime() - date.getTime()
129
+ if (elapsed < MS_PER_DAY) {
130
+ const hours = elapsed <= 0 ? 0 : Math.max(1, Math.floor(elapsed / MS_PER_HOUR))
131
+ return n('nextcloud-vue', '%n hour', '%n hours', hours)
132
+ }
133
+ const days = shownAgeDays(date, now)
134
+ return n('nextcloud-vue', '%n day', '%n days', days)
135
+ }
136
+
137
+ /**
138
+ * The number of days an age cell shows: 0 under 24 hours (the cell then reads
139
+ * hours), else the whole calendar days, at least 1. The threshold rules
140
+ * compare against this number, so the colour always agrees with the text.
141
+ *
142
+ * @param {unknown} value The moment the wait started.
143
+ * @param {Date} [now] The reference moment.
144
+ * @return {number|null} The day count, or null when the value is not a date.
145
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-turns-red-past-its-threshold
146
+ */
147
+ export function shownAgeDays(value, now = new Date()) {
148
+ const date = parseDateValue(value)
149
+ if (date === null) {
150
+ return null
151
+ }
152
+ if (now.getTime() - date.getTime() < MS_PER_DAY) {
153
+ return 0
154
+ }
155
+ return Math.max(1, ageInDays(date, now))
156
+ }
157
+
158
+ /**
159
+ * The variant of the first `variantWhen` rule the age in days matches, or ''
160
+ * when none does. Same rule shape as the `date` widget (`{ op, value,
161
+ * variant }`, first match wins), compared against the number of days the cell
162
+ * shows (`shownAgeDays`): `[{ op: "gte", value: 3, variant: "error" }]` turns a wait of three
163
+ * days or more red.
164
+ *
165
+ * @param {unknown} value The moment the wait started.
166
+ * @param {Array<{op: string, value: number, variant: string}>} rules The rules.
167
+ * @param {Date} [now] The reference moment.
168
+ * @return {string} `success | warning | error | default`, or ''.
169
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-turns-red-past-its-threshold
170
+ */
171
+ export function resolveAgeVariant(value, rules, now = new Date()) {
172
+ if (!Array.isArray(rules) || rules.length === 0) {
173
+ return ''
174
+ }
175
+ const days = shownAgeDays(value, now)
176
+ if (days === null) {
177
+ return ''
178
+ }
179
+ for (const rule of rules) {
180
+ if (!rule || typeof rule !== 'object') {
181
+ continue
182
+ }
183
+ if (compareVisibleWhen(days, rule.op || 'eq', rule.value) === true) {
184
+ const variant = rule.variant === 'danger' ? 'error' : rule.variant
185
+ return AGE_VARIANTS.includes(variant) ? variant : ''
186
+ }
187
+ }
188
+ return ''
189
+ }
@@ -101,6 +101,7 @@ const DEFAULT_MAP = {
101
101
  enum: 'enumValues',
102
102
  required: 'isRequired',
103
103
  default: 'defaultValue',
104
+ hideOnCreate: 'hideOnCreate',
104
105
  }
105
106
 
106
107
  /**
@@ -312,6 +313,8 @@ export function definitionQueryParams(config, value, formData = {}) {
312
313
  * @param {object} [config] The `x-openregister-extends-form` block (`map`, `typeMap`).
313
314
  * @param {object} [options] Extra options.
314
315
  * @param {number} [options.orderFrom] Order assigned to the first field; each subsequent field takes the next integer. Dynamic fields sort after the schema's own, so pass a number above every declared `order`.
316
+ * @param {boolean} [options.create] The form creates a new object: a definition whose `hideOnCreate` (or the field `config.map.hideOnCreate` names) is `true` gets no field, because the app fills that answer itself at creation. Edit forms still show it.
317
+ * @spec openspec/changes/dynamic-question-hide-on-create/specs/dynamic-question-hide-on-create/spec.md#requirement-a-question-can-stay-off-the-create-form
315
318
  * @return {{properties: object, required: string[]}} Properties keyed by prefixed id, plus the required key list.
316
319
  */
317
320
  export function propertiesFromDefinitions(definitions, config = {}, options = {}) {
@@ -321,7 +324,8 @@ export function propertiesFromDefinitions(definitions, config = {}, options = {}
321
324
 
322
325
  const properties = {}
323
326
  const required = []
324
- const list = Array.isArray(definitions) ? definitions : []
327
+ const list = (Array.isArray(definitions) ? definitions : [])
328
+ .filter((record) => !(options.create === true && record && mapped(record, map, 'hideOnCreate') === true))
325
329
 
326
330
  list.forEach((record, index) => {
327
331
  if (!record || !record.id) {