@conduction/nextcloud-vue 2.105.0 → 2.106.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 (153) hide show
  1. package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
  2. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +18 -0
  3. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  4. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -1
  5. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +18 -0
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  9. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +1 -1
  10. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  11. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +53 -4
  12. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  14. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +9 -0
  15. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js.map +1 -0
  16. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +24 -0
  17. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -0
  18. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +49 -0
  19. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -0
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +36 -18
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +17 -38
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  26. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  28. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  29. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +53 -2
  30. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  31. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +119 -111
  32. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue.js +1 -1
  34. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +62 -1
  35. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +159 -50
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +121 -98
  40. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
  42. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +24 -0
  43. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +20 -4
  45. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
  46. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +22 -4
  47. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  48. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +112 -1
  49. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  50. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +100 -44
  51. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  52. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  53. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +67 -4
  54. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  55. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
  56. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  58. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +295 -12
  59. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  60. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +267 -144
  61. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +4 -0
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +3 -1
  66. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +93 -1
  69. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  70. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +322 -246
  71. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  72. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue.js +1 -1
  73. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +122 -7
  74. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +189 -93
  76. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  77. package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
  78. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  79. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  80. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  81. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +42 -0
  82. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  83. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +38 -19
  84. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  85. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue.js +9 -0
  86. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue.js.map +1 -0
  87. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue2.js +258 -0
  88. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue2.js.map +1 -0
  89. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue3.js +69 -0
  90. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue3.js.map +1 -0
  91. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
  92. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  93. package/dist/esm/index.js +1 -0
  94. package/dist/esm/index.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +21 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +21 -1
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/library-style.js +1 -1
  100. package/dist/esm/nextcloud-vue.css +803 -453
  101. package/dist/esm/utils/dueText.js +18 -3
  102. package/dist/esm/utils/dueText.js.map +1 -1
  103. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  104. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  105. package/dist/esm/utils/sortOrder.js +11 -1
  106. package/dist/esm/utils/sortOrder.js.map +1 -1
  107. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  108. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  109. package/dist/nextcloud-vue.cjs.js +8281 -6350
  110. package/dist/nextcloud-vue.cjs.js.map +2 -2
  111. package/dist/nextcloud-vue.css +803 -453
  112. package/l10n/en.json +21 -1
  113. package/l10n/nl.json +21 -1
  114. package/package.json +1 -1
  115. package/src/components/CnCardGrid/CnCardGrid.md +2 -0
  116. package/src/components/CnCardGrid/CnCardGrid.vue +20 -0
  117. package/src/components/CnCellRenderer/CnCellRenderer.vue +19 -1
  118. package/src/components/CnDetailPage/CnDetailPage.md +2 -2
  119. package/src/components/CnDetailPage/CnDetailPage.vue +53 -4
  120. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +60 -0
  121. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +36 -29
  122. package/src/components/CnFactsWidget/CnFactsWidget.vue +4 -0
  123. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +70 -4
  124. package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +62 -1
  125. package/src/components/CnIndexPage/CnIndexPage.md +7 -5
  126. package/src/components/CnIndexPage/CnIndexPage.vue +187 -58
  127. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +36 -1
  128. package/src/components/CnIndexPage/useSelfFetchList.js +22 -4
  129. package/src/components/CnIndexSidebar/CnIndexSidebar.md +2 -0
  130. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +148 -1
  131. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +68 -5
  132. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  133. package/src/components/CnObjectCard/CnObjectCard.vue +354 -13
  134. package/src/components/CnObjectSidebar/CnFilesTab.vue +6 -0
  135. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +190 -4
  136. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +209 -9
  137. package/src/components/CnTabs/CnTabs.vue +11 -0
  138. package/src/components/CnTabsWidget/CnTabsWidget.vue +91 -18
  139. package/src/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.md +11 -0
  140. package/src/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue +382 -0
  141. package/src/components/CnTeamWorkloadWidget/index.js +14 -0
  142. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
  143. package/src/components/index.js +1 -0
  144. package/src/css/index-page.css +14 -2
  145. package/src/css/look-board-detail.css +48 -4
  146. package/src/css/look-board-index.css +64 -2
  147. package/src/css/look-board.css +25 -2
  148. package/src/index.js +1 -1
  149. package/src/schemas/app-manifest-v2.schema.json +75 -6
  150. package/src/utils/dueText.js +18 -3
  151. package/src/utils/libraryWidgetKeys.js +1 -0
  152. package/src/utils/sortOrder.js +10 -0
  153. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -15,8 +15,61 @@
15
15
  @update:modelValue="$emit('select', object)" />
16
16
  </div>
17
17
 
18
+ <!-- Board look, record layout (`layout`): head pill and number, title,
19
+ meta line, then a footer with a pill and a date. -->
20
+ <div v-if="recordLayout" class="cn-object-card__content cn-object-card__record" data-testid="cn-object-card-record">
21
+ <div class="cn-object-card__record-head">
22
+ <CnStatusBadge
23
+ v-if="recordHeadPill"
24
+ class="cn-object-card__record-pill"
25
+ :label="recordHeadPill.label"
26
+ :variant="recordHeadPill.variant"
27
+ data-testid="cn-object-card-head-pill" />
28
+ <span v-if="recordNumber" class="cn-object-card__record-number" data-testid="cn-object-card-number">{{ recordNumber }}</span>
29
+ <div
30
+ v-if="$slots.actions"
31
+ class="cn-object-card__menu"
32
+ @click.stop
33
+ @auxclick.stop>
34
+ <slot name="actions" :object="object" />
35
+ </div>
36
+ </div>
37
+ <h3 class="cn-object-card__title">
38
+ <CnLockIndicator :object="object" :size="16" />
39
+ {{ recordTitle }}
40
+ </h3>
41
+ <!-- A part whose field names a page column with a widget draws through
42
+ it, as its table cell does; the rest are text. -->
43
+ <p v-if="recordMetaParts.length > 0" class="cn-object-card__record-meta" data-testid="cn-object-card-meta">
44
+ <template v-for="(part, index) in recordMetaParts" :key="part.field">
45
+ <span v-if="index > 0" aria-hidden="true"> · </span>
46
+ <CnCellRenderer
47
+ v-if="part.column && part.column.widget"
48
+ :value="part.value"
49
+ :widget="part.column.widget"
50
+ :widgetProps="part.column.widgetProps || undefined"
51
+ :formatter="part.column.formatter || null"
52
+ :formatterOptions="part.column.formatterOptions || null"
53
+ :row="object" />
54
+ <span v-else>{{ part.text }}</span>
55
+ </template>
56
+ </p>
57
+ <div v-if="$slots.badges" class="cn-object-card__badges">
58
+ <slot name="badges" :object="object" />
59
+ </div>
60
+ <div v-if="recordFootPill || recordDate" class="cn-object-card__record-foot" data-testid="cn-object-card-foot">
61
+ <CnStatusBadge
62
+ v-if="recordFootPill"
63
+ class="cn-object-card__record-pill"
64
+ :label="recordFootPill.label"
65
+ :variant="recordFootPill.variant"
66
+ data-testid="cn-object-card-foot-pill" />
67
+ <span v-if="recordDate" class="cn-object-card__record-date" data-testid="cn-object-card-date">{{ recordDate }}</span>
68
+ </div>
69
+ </div>
70
+
18
71
  <!-- Card content -->
19
- <div class="cn-object-card__content">
72
+ <div v-else class="cn-object-card__content">
20
73
  <!-- Header: image + title -->
21
74
  <div class="cn-object-card__header">
22
75
  <!-- Board look: the leading element, initials in a circle or an icon on a tint. -->
@@ -92,6 +145,11 @@
92
145
  <CnCellRenderer
93
146
  :value="field.value"
94
147
  :property="field.property"
148
+ :formatter="field.column && field.column.formatter || null"
149
+ :formatterOptions="field.column && field.column.formatterOptions || null"
150
+ :widget="field.column && field.column.widget || null"
151
+ :widgetProps="field.column && field.column.widgetProps || undefined"
152
+ :row="object"
95
153
  :truncate="60" />
96
154
  </div>
97
155
  </slot>
@@ -127,9 +185,15 @@
127
185
  </template>
128
186
 
129
187
  <script>
188
+ import { translate as t } from '@nextcloud/l10n'
130
189
  import { NcButton, NcCheckboxRadioSwitch } from '@nextcloud/vue'
131
190
  import { useClickDragGuard } from '../../composables/useClickDragGuard.js'
132
191
  import { normalizeLook } from '../../composables/useLook.js'
192
+ import { isBadgeVariant } from '../../utils/badgeVariants.js'
193
+ import { formatBoardDate } from '../../utils/boardDate.js'
194
+ import { resolveDateVariant } from '../../utils/dateVariant.js'
195
+ import { resolveDue } from '../../utils/dueText.js'
196
+ import { readPath } from '../../utils/readPath.js'
133
197
  import { resolveImageUrl } from '../../utils/resolveImageUrl.js'
134
198
  import { isRowMiddleClick, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
135
199
  import { formatValue } from '../../utils/schema.js'
@@ -178,6 +242,8 @@ export default {
178
242
  cnTranslate: { default: () => (key) => key },
179
243
  /** The look CnAppRoot provides; `board` draws the record card of the screens. */
180
244
  cnLook: { default: 'nextcloud' },
245
+ /** The cell formatters CnAppRoot provides, for a layout part whose page column names one. */
246
+ cnFormatters: { default: () => ({}) },
181
247
  },
182
248
 
183
249
  props: {
@@ -270,6 +336,25 @@ export default {
270
336
  default: null,
271
337
  },
272
338
 
339
+ /**
340
+ * Board look: the record layout (manifest key `config.cardLayout`).
341
+ * Every part names a field of the object (a dot path) and is left out
342
+ * while that field is empty:
343
+ * `{ headPill, number, title, meta, footerPill, footerDate }`.
344
+ * A pill is `{ field, colorMap?, labels?, variant? }`: `colorMap` maps a
345
+ * value to a CnStatusBadge variant (ignoring case), `labels` a value to
346
+ * the text shown. `meta` is a list of fields, joined with " · ".
347
+ * `footerDate` is `{ field, label? }`, drawn as "Termijn 5 okt".
348
+ * Without it the card keeps its head, facts and action.
349
+ *
350
+ * @type {{headPill?: object, number?: string, title?: string, meta?: Array<string>, footerPill?: object, footerDate?: {field: string, label?: string}}}
351
+ * @spec openspec/changes/board-record-card-layout/specs/board-record-card-layout/spec.md#requirement-a-board-card-can-draw-the-record-layout-from-config
352
+ */
353
+ layout: {
354
+ type: Object,
355
+ default: null,
356
+ },
357
+
273
358
  /**
274
359
  * The property keys the facts list shows, in order (manifest key
275
360
  * `config.cardFields`). Without it the card shows the first
@@ -282,6 +367,17 @@ export default {
282
367
  default: null,
283
368
  },
284
369
 
370
+ /**
371
+ * The page's columns by key: a fact whose key names a column renders
372
+ * through that column's label, formatter and widget, as the table does.
373
+ *
374
+ * @type {object}
375
+ */
376
+ cardFieldColumns: {
377
+ type: Object,
378
+ default: null,
379
+ },
380
+
285
381
  /** Maximum number of metadata fields to show */
286
382
  maxMetadata: {
287
383
  type: Number,
@@ -333,6 +429,92 @@ export default {
333
429
  return null
334
430
  },
335
431
 
432
+ /**
433
+ * The record layout when the board look draws it, else null.
434
+ *
435
+ * @return {?object}
436
+ * @spec openspec/changes/board-record-card-layout/specs/board-record-card-layout/spec.md#requirement-a-board-card-can-draw-the-record-layout-from-config
437
+ */
438
+ recordLayout() {
439
+ return this.isBoardLook && this.layout && typeof this.layout === 'object' ? this.layout : null
440
+ },
441
+
442
+ /**
443
+ * The record layout's head pill, or null.
444
+ *
445
+ * @return {?{label: string, variant: string}}
446
+ */
447
+ recordHeadPill() {
448
+ return this.recordLayout ? this.layoutPill(this.recordLayout.headPill) : null
449
+ },
450
+
451
+ /**
452
+ * The record layout's footer pill, or null.
453
+ *
454
+ * @return {?{label: string, variant: string}}
455
+ */
456
+ recordFootPill() {
457
+ return this.recordLayout ? this.layoutPill(this.recordLayout.footerPill) : null
458
+ },
459
+
460
+ /**
461
+ * The record number at the end of the head, or ''.
462
+ *
463
+ * @return {string}
464
+ */
465
+ recordNumber() {
466
+ return this.recordLayout ? this.layoutText(this.recordLayout.number) : ''
467
+ },
468
+
469
+ /**
470
+ * The title: the layout's `title` field, else the card's own title.
471
+ *
472
+ * @return {string}
473
+ */
474
+ recordTitle() {
475
+ const text = this.recordLayout ? this.layoutText(this.recordLayout.title) : ''
476
+ return text !== '' ? text : this.title
477
+ },
478
+
479
+ /**
480
+ * The meta line: the layout's `meta` fields that hold a value, joined.
481
+ *
482
+ * @return {string}
483
+ */
484
+ recordMeta() {
485
+ return this.recordMetaParts.map((part) => part.text).join(' · ')
486
+ },
487
+
488
+ /**
489
+ * The meta line's parts that hold a value: each with its text and, when
490
+ * its field names a page column, that column (whose widget draws it).
491
+ *
492
+ * @return {Array<{field: string, value: unknown, text: string, column: ?object}>}
493
+ */
494
+ recordMetaParts() {
495
+ const fields = this.recordLayout && Array.isArray(this.recordLayout.meta) ? this.recordLayout.meta : []
496
+ return fields
497
+ .map((part) => this.metaPart(part))
498
+ .filter((part) => part !== null && part.text !== '')
499
+ },
500
+
501
+ /**
502
+ * The footer date with its label ("Termijn 5 okt"), or ''.
503
+ *
504
+ * @return {string}
505
+ */
506
+ recordDate() {
507
+ const def = this.recordLayout ? this.recordLayout.footerDate : null
508
+ if (!def || typeof def.field !== 'string') {
509
+ return ''
510
+ }
511
+ const date = formatBoardDate(readPath(this.object, def.field))
512
+ if (date === '') {
513
+ return ''
514
+ }
515
+ return typeof def.label === 'string' && def.label !== '' ? `${this.cnTranslate(def.label)} ${date}` : date
516
+ },
517
+
336
518
  /**
337
519
  * The accent variant when it is a known one, else ''.
338
520
  *
@@ -401,12 +583,7 @@ export default {
401
583
  if (Array.isArray(this.cardFields)) {
402
584
  return this.cardFields
403
585
  .filter((key) => typeof key === 'string' && this.schema.properties[key] && !this.configFields.includes(key))
404
- .map((key) => ({
405
- key,
406
- label: this.cnTranslate(this.schema.properties[key].title || key),
407
- value: this.object[key],
408
- property: this.schema.properties[key],
409
- }))
586
+ .map((key) => this.metadataField(key, this.schema.properties[key]))
410
587
  }
411
588
 
412
589
  return Object.entries(this.schema.properties)
@@ -431,16 +608,180 @@ export default {
431
608
  return orderA - orderB
432
609
  })
433
610
  .slice(0, this.maxMetadata)
434
- .map(([key, prop]) => ({
435
- key,
436
- label: this.cnTranslate(prop.title || key),
437
- value: this.object[key],
438
- property: prop,
439
- }))
611
+ .map(([key, prop]) => this.metadataField(key, prop))
440
612
  },
441
613
  },
442
614
 
443
615
  methods: {
616
+ /**
617
+ * One meta part: a field name reads through the page column it names;
618
+ * `{ field, formatter?, formatterOptions? }` reads as plain text through
619
+ * its own formatter, so a column's widget (a badge) stays in the table.
620
+ *
621
+ * @param {string|{field: string, formatter?: string, formatterOptions?: object}} part The part.
622
+ * @return {?{field: string, value: unknown, text: string, column: ?object}} The part, or null when it names no field.
623
+ * @spec openspec/changes/card-layout-part-formatter/specs/card-layout-part-formatter/spec.md#requirement-a-card-meta-part-can-name-its-own-formatter
624
+ */
625
+ metaPart(part) {
626
+ if (typeof part === 'string' && part !== '') {
627
+ return { field: part, value: readPath(this.object, part), text: this.layoutText(part), column: this.layoutColumn(part) }
628
+ }
629
+ if (!part || typeof part !== 'object' || typeof part.field !== 'string' || part.field === '') {
630
+ return null
631
+ }
632
+ const column = typeof part.formatter === 'string' ? { formatter: part.formatter, formatterOptions: part.formatterOptions } : null
633
+ return { field: part.field, value: readPath(this.object, part.field), text: this.layoutText(part.field, column), column }
634
+ },
635
+
636
+ /**
637
+ * A field's value as text: a person or a record by its display name,
638
+ * title or name, anything else as a string. '' for an empty value or
639
+ * a field that is not named.
640
+ *
641
+ * @param {unknown} field A dot path into the object.
642
+ * @param {?object} [own] The column to read through instead of the one the field names.
643
+ * @return {string}
644
+ */
645
+ layoutText(field, own) {
646
+ if (typeof field !== 'string' || field === '') {
647
+ return ''
648
+ }
649
+ const value = readPath(this.object, field)
650
+ if (value === null || value === undefined || value === '') {
651
+ return ''
652
+ }
653
+ // A part whose field names a page column with a formatter reads as
654
+ // its table cell does (a case type uuid as its name).
655
+ const column = own === undefined ? this.layoutColumn(field) : own
656
+ const format = column && column.formatter && this.cnFormatters ? this.cnFormatters[column.formatter] : null
657
+ if (typeof format === 'function') {
658
+ const formatted = format(value, this.object, this.schema?.properties?.[field], column.formatterOptions || undefined)
659
+ return formatted === null || formatted === undefined ? '' : String(formatted)
660
+ }
661
+ if (typeof value === 'object') {
662
+ const name = value.displayName || value.title || value.name
663
+ return typeof name === 'string' ? name : ''
664
+ }
665
+ return String(value)
666
+ },
667
+
668
+ /**
669
+ * The page column a layout part's field names, or null.
670
+ *
671
+ * @param {string} field The part's field.
672
+ * @return {?object} The column.
673
+ */
674
+ layoutColumn(field) {
675
+ return (this.cardFieldColumns && typeof field === 'string' && this.cardFieldColumns[field]) || null
676
+ },
677
+
678
+ /**
679
+ * A pill that reads its date field through the due rules: `variantWhen`
680
+ * (the first rule the days left match gives the tone), else `due`
681
+ * (overdue reads as error, within `due.soonWithin` days as warning).
682
+ * The label is `labels[variant]`, else `labels.overdue` / `labels.soon`,
683
+ * else Overdue / Soon, through the host's translate. Any other date
684
+ * draws no pill.
685
+ *
686
+ * @param {{field: string, due?: object, variantWhen?: Array, labels?: object}} def The pill.
687
+ * @return {?{label: string, variant: string}}
688
+ */
689
+ datePill(def) {
690
+ const value = readPath(this.object, def.field)
691
+ const variant = Array.isArray(def.variantWhen)
692
+ ? this.dateRuleVariant(value, def.variantWhen)
693
+ : this.dueVariant(value, def.due)
694
+ if (variant === '') {
695
+ return null
696
+ }
697
+ const labels = def.labels && typeof def.labels === 'object' ? def.labels : {}
698
+ const state = variant === 'error' ? 'overdue' : (variant === 'warning' ? 'soon' : '')
699
+ const authored = labels[variant] || (state && labels[state])
700
+ if (typeof authored === 'string' && authored !== '') {
701
+ return { label: this.cnTranslate(authored), variant }
702
+ }
703
+ if (state === 'overdue') {
704
+ return { label: t('nextcloud-vue', 'Overdue'), variant }
705
+ }
706
+ if (state === 'soon') {
707
+ return { label: t('nextcloud-vue', 'Soon'), variant }
708
+ }
709
+ return null
710
+ },
711
+
712
+ /**
713
+ * The variant of the first `variantWhen` rule a date matches, or ''.
714
+ *
715
+ * @param {unknown} value The date.
716
+ * @param {Array} rules The rules.
717
+ * @return {string} The variant, or ''.
718
+ */
719
+ dateRuleVariant(value, rules) {
720
+ const matched = resolveDateVariant(value, rules)
721
+ return matched === 'default' ? '' : matched
722
+ },
723
+
724
+ /**
725
+ * The variant the due rules give a date: error when overdue, warning
726
+ * within `soonWithin` days, else ''.
727
+ *
728
+ * @param {unknown} value The date.
729
+ * @param {object} config The `due` config.
730
+ * @return {string} The variant, or ''.
731
+ */
732
+ dueVariant(value, config) {
733
+ const due = resolveDue(value, config)
734
+ if (due && due.tone === 'danger') {
735
+ return 'error'
736
+ }
737
+ return due && due.soon ? 'warning' : ''
738
+ },
739
+
740
+ /**
741
+ * A layout pill as `{ label, variant }`, or null while its field is
742
+ * empty.
743
+ *
744
+ * @param {?{field: string, colorMap?: object, labels?: object, variant?: string}} def The pill.
745
+ * @return {?{label: string, variant: string}}
746
+ */
747
+ layoutPill(def) {
748
+ if (!def || typeof def !== 'object') {
749
+ return null
750
+ }
751
+ if (def.due || Array.isArray(def.variantWhen)) {
752
+ return this.datePill(def)
753
+ }
754
+ const value = this.layoutText(def.field)
755
+ if (value === '') {
756
+ return null
757
+ }
758
+ const labels = def.labels && typeof def.labels === 'object' ? def.labels : {}
759
+ const label = typeof labels[value] === 'string' ? this.cnTranslate(labels[value]) : value
760
+ const colorMap = def.colorMap && typeof def.colorMap === 'object' ? def.colorMap : {}
761
+ const key = Object.keys(colorMap).find((k) => k.toLowerCase() === value.toLowerCase())
762
+ const variant = key !== undefined ? colorMap[key] : def.variant
763
+ return { label, variant: isBadgeVariant(variant) ? variant : 'default' }
764
+ },
765
+
766
+ /**
767
+ * One fact of the card: the property's value with the matching page
768
+ * column (label, formatter, widget) when there is one.
769
+ *
770
+ * @param {string} key The property key.
771
+ * @param {object} prop The schema property.
772
+ * @return {object} { key, label, value, property, column }
773
+ */
774
+ metadataField(key, prop) {
775
+ const column = (this.cardFieldColumns && this.cardFieldColumns[key]) || null
776
+ return {
777
+ key,
778
+ label: this.cnTranslate((column && column.label) || prop.title || key),
779
+ value: this.object[key],
780
+ property: prop,
781
+ column,
782
+ }
783
+ },
784
+
444
785
  formatValue,
445
786
 
446
787
  /**
@@ -12,6 +12,8 @@
12
12
  :deleteLabel="deleteLabel"
13
13
  :uploadButton="uploadButton"
14
14
  :dropOverlay="dropOverlay"
15
+ :dropLabel="dropLabel"
16
+ :dropDetail="dropDetail"
15
17
  :dropHint="dropHint"
16
18
  :rowActions="rowActions"
17
19
  :newActions="newActions"
@@ -347,6 +349,10 @@ export default {
347
349
  uploadButton: { type: Boolean, default: false },
348
350
  /** Forwarded to CnFilesBrowser: the drop state drawn over the list ("Drop to add") and announced. */
349
351
  dropOverlay: { type: Boolean, default: false },
352
+ /** Forwarded to CnFilesBrowser: the drop state's line ("Laat los om toe te voegen"); the library's own without it. */
353
+ dropLabel: { type: String, default: undefined },
354
+ /** Forwarded to CnFilesBrowser: the drop state's second line, `{count}` the files dragged; it also makes the overlay fill the panel. */
355
+ dropDetail: { type: String, default: '' },
350
356
  /** Forwarded to CnFilesBrowser: "Or drag files onto this list." under the list. */
351
357
  dropHint: { type: Boolean, default: false },
352
358
  /**