@conduction/nextcloud-vue 2.84.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.
Files changed (166) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  7. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
  8. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  9. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
  10. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  11. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  12. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +114 -0
  13. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +2 -2
  15. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +45 -0
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +23 -4
  22. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  25. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
  26. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  28. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  29. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
  30. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  31. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
  33. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
  37. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  38. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  39. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +95 -0
  40. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  41. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
  42. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  43. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  44. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +70 -2
  45. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  47. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  48. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
  49. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  50. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
  51. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  52. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  53. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
  54. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  55. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  56. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  57. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +134 -10
  58. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
  60. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  61. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  62. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +6 -3
  63. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  64. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
  66. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
  67. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
  68. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  70. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
  71. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  72. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
  73. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  74. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
  75. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
  76. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  77. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  78. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
  79. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
  80. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
  81. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
  82. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  83. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
  84. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  85. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  86. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  87. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
  88. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  89. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
  90. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  91. package/dist/esm/composables/useListView.js +21 -0
  92. package/dist/esm/composables/useListView.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 +23 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +41 -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 +742 -397
  101. package/dist/esm/store/useObjectStore.js +16 -2
  102. package/dist/esm/store/useObjectStore.js.map +1 -1
  103. package/dist/esm/utils/boardColumns.js +8 -1
  104. package/dist/esm/utils/boardColumns.js.map +1 -1
  105. package/dist/esm/utils/boardReferenceColumns.js +278 -0
  106. package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
  107. package/dist/esm/utils/buildManifest.js +4 -1
  108. package/dist/esm/utils/buildManifest.js.map +1 -1
  109. package/dist/esm/utils/gridStack.js +40 -1
  110. package/dist/esm/utils/gridStack.js.map +1 -1
  111. package/dist/esm/utils/humaniseKey.js +41 -0
  112. package/dist/esm/utils/humaniseKey.js.map +1 -0
  113. package/dist/esm/utils/notificationRuleLabel.js +6 -23
  114. package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
  115. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  116. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  117. package/dist/esm/utils/widgetDispatch.js +8 -2
  118. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  119. package/dist/nextcloud-vue.cjs.js +15075 -12590
  120. package/dist/nextcloud-vue.cjs.js.map +2 -2
  121. package/dist/nextcloud-vue.css +742 -397
  122. package/l10n/en.json +23 -1
  123. package/l10n/nl.json +41 -1
  124. package/package.json +1 -1
  125. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
  126. package/src/components/CnBoardView/CnBoardView.vue +262 -6
  127. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +116 -2
  128. package/src/components/CnDashboardPage/CnDashboardPage.vue +57 -2
  129. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  130. package/src/components/CnDetailPage/CnDetailPage.vue +23 -4
  131. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
  132. package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
  133. package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
  134. package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
  135. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +123 -3
  136. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +70 -2
  137. package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
  138. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
  139. package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
  140. package/src/components/CnStatWidget/CnStatWidget.vue +136 -11
  141. package/src/components/CnTabsWidget/CnTabsWidget.vue +6 -3
  142. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
  143. package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
  144. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
  145. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
  146. package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
  147. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
  148. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
  149. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
  150. package/src/composables/useListView.js +21 -0
  151. package/src/css/board.css +65 -4
  152. package/src/css/dialog.css +4 -2
  153. package/src/css/index-page.css +49 -0
  154. package/src/css/kpi-card.css +13 -0
  155. package/src/css/look-board.css +96 -7
  156. package/src/index.js +2 -0
  157. package/src/schemas/app-manifest-v2.schema.json +115 -5
  158. package/src/store/useObjectStore.js +16 -2
  159. package/src/utils/boardColumns.js +8 -1
  160. package/src/utils/boardReferenceColumns.js +274 -0
  161. package/src/utils/buildManifest.js +4 -1
  162. package/src/utils/gridStack.js +39 -0
  163. package/src/utils/humaniseKey.js +38 -0
  164. package/src/utils/notificationRuleLabel.js +5 -23
  165. package/src/utils/validateManifestV2.compiled.js +3 -3
  166. package/src/utils/widgetDispatch.js +8 -2
@@ -82,7 +82,7 @@
82
82
  </span>
83
83
  </div>
84
84
 
85
- <div class="cn-kpi-card__value-row cn-stat-widget__value-row">
85
+ <div ref="valueRow" class="cn-kpi-card__value-row cn-stat-widget__value-row">
86
86
  <NcLoadingIcon v-if="displayLoading" :size="22" />
87
87
  <span v-else-if="displayError" class="cn-stat-widget__error" :title="displayError">—</span>
88
88
  <template v-else>
@@ -96,6 +96,7 @@
96
96
  :variant="badgeVariant" />
97
97
  <span
98
98
  v-else
99
+ ref="value"
99
100
  class="cn-kpi-card__value cn-stat-widget__value"
100
101
  :data-testid="isCountdown ? 'cn-stat-widget-countdown' : null"
101
102
  :class="{ 'cn-kpi-card__value--text': isTextValue || shownValueIsText }"
@@ -165,7 +166,7 @@ import { resolveObjectOpType } from '../../utils/actionsDispatcher.js'
165
166
  import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
166
167
  import { aggFilterParam } from '../../utils/fetchAggregate.js'
167
168
  import { fetchListTotal, needsListEndpoint } from '../../utils/fetchFilterCounts.js'
168
- import { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'
169
+ import { formatMetricValue, metricLocale, unwrapAppConfig } from '../../utils/formatMetric.js'
169
170
  import { dropOptionalUnresolved, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
170
171
  import { STATUS_TEXT_COLORS } from '../../utils/statusColors.js'
171
172
  import { evaluateVisibleWhenLocal, readVisibleWhenPath } from '../../utils/visibleWhen.js'
@@ -223,6 +224,9 @@ const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/
223
224
  /** Milliseconds in a day, used only between two UTC-normalised midnights. */
224
225
  const DAY_MS = 86400000
225
226
 
227
+ /** The smallest size `fitValue` steps a number down to, in px; below it the number wraps. */
228
+ const MIN_FIT_FONT_SIZE = 14
229
+
226
230
  /**
227
231
  * The local calendar day a value names, as a UTC midnight timestamp.
228
232
  *
@@ -517,7 +521,7 @@ export default {
517
521
  * counted between LOCAL CALENDAR DAYS, not as elapsed milliseconds, so
518
522
  * the time of day on a deadline never moves the answer.
519
523
  *
520
- * @type {{label?: string, icon?: string, iconColor?: string, valueColor?: string, caption?: string, route?: (object|string), clickRoute?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {kind?: string, register?: string, schema?: string, metric?: string, field?: string, filter?: object, url?: string, path?: string, params?: object}, endpointSource?: {url: string, method?: string, params?: object, responsePath?: string}, valueField?: string, limitField?: string, limit?: number, dateRange?: {presets?: Array<{id: string, label?: string, from?: string, to?: string}>, default?: string}, previousField?: string, deltaField?: string, goodDirection?: ('up'|'down'), variant?: ('default'|'primary'|'success'|'warning'|'error'|'danger'), variantWhen?: Array<{op: string, value: unknown, variant: string, icon?: string}>, objectField?: (string|{field: string, resolve?: {register: string, schema: string, labelField?: string, variantField?: string, variantMap?: {[key: string]: string}}}), display?: ('text'|'badge'|'countdown'), countdown?: {unit?: 'days', warnAt?: number, dangerAt?: number, futureLabel?: string, todayLabel?: string, pastLabel?: string, emptyText?: string}, emptyText?: string, captionVariant?: ('default'|'primary'|'success'|'warning'|'error'|'danger'), captionVariantWhen?: Array<{op: string, value: unknown, variant: string}>, captionSource?: {register?: string, schema?: string, metric?: string, field?: string, filter?: object, via?: string, zeroText?: string}, overrides?: Array<{when: {field: string, op?: string, value?: unknown}, label?: string, variant?: string, icon?: string, caption?: string, captionVariant?: string}>}}
524
+ * @type {{label?: string, icon?: string, iconColor?: string, valueColor?: string, caption?: string, route?: (object|string), clickRoute?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {kind?: string, register?: string, schema?: string, metric?: string, field?: string, filter?: object, url?: string, path?: string, params?: object}, endpointSource?: {url: string, method?: string, params?: object, responsePath?: string}, valueField?: string, limitField?: string, limit?: number, dateRange?: {presets?: Array<{id: string, label?: string, from?: string, to?: string}>, default?: string}, previousField?: string, deltaField?: string, goodDirection?: ('up'|'down'), variant?: ('default'|'primary'|'success'|'warning'|'error'|'danger'), variantWhen?: Array<{op: string, value: unknown, variant: string, icon?: string}>, objectField?: (string|{field: string, resolve?: {register: string, schema: string, labelField?: string, variantField?: string, variantMap?: {[key: string]: string}}}), display?: ('text'|'badge'|'countdown'), countdown?: {unit?: 'days', warnAt?: number, dangerAt?: number, futureLabel?: string, todayLabel?: string, pastLabel?: string, emptyText?: string}, emptyText?: string, captionVariant?: ('default'|'primary'|'success'|'warning'|'error'|'danger'), captionVariantWhen?: Array<{op: string, value: unknown, variant: string}>, captionSource?: {kind?: 'ratio', register?: string, schema?: string, metric?: string, field?: string, filter?: object, via?: string, zeroText?: string, numerator?: {metric?: string, field?: string, filter?: object, via?: string}, denominator?: {metric?: string, field?: string, filter?: object, via?: string}, decimals?: number}, overrides?: Array<{when: {field: string, op?: string, value?: unknown}, label?: string, variant?: string, icon?: string, caption?: string, captionVariant?: string}>}}
521
525
  */
522
526
  content: {
523
527
  type: Object,
@@ -592,6 +596,9 @@ export default {
592
596
 
593
597
  data() {
594
598
  return {
599
+ // The value's font size while the number would not fit its row at
600
+ // its own size; null when it fits.
601
+ fitFontSize: null,
595
602
  // Resolved display label for an `objectField` that holds a reference
596
603
  // uuid. Null until resolved, and null forever when it cannot be.
597
604
  referenceLabel: null,
@@ -609,6 +616,9 @@ export default {
609
616
  value: null,
610
617
  // The caption's own count (`content.captionSource`), null until read.
611
618
  captionValue: null,
619
+ // The parts of a ratio caption (`captionSource.kind: "ratio"`): the
620
+ // numerator read (null: the tile's own value) and the denominator.
621
+ captionRatioParts: null,
612
622
  loading: false,
613
623
  error: '',
614
624
  }
@@ -684,8 +694,9 @@ export default {
684
694
  // nothing to say while it is not read, and at zero the `zeroText`
685
695
  // (or nothing) instead of "0 waiting".
686
696
  const counted = this.hasCaptionSource
687
- if (counted && !(Number(this.captionValue) > 0)) {
688
- const zero = this.captionValue !== null && this.content.captionSource.zeroText
697
+ const count = this.captionCount
698
+ if (counted && !(Number(count) > 0)) {
699
+ const zero = count !== null && this.content.captionSource.zeroText
689
700
  return typeof zero === 'string' && zero !== '' ? this.effectiveTranslate(zero) : ''
690
701
  }
691
702
  const translated = this.effectiveTranslate(caption)
@@ -694,9 +705,10 @@ export default {
694
705
  }
695
706
  const payload = this.endpointMode ? this.epData : null
696
707
  return translated.replace(/\{([A-Za-z0-9_.]+)\}/g, (whole, path) => {
697
- // `{count}` is the caption's own count.
698
- if (path === 'count' && counted) {
699
- return String(this.captionValue)
708
+ // `{count}` is the caption's own count; `{ratio}` the same
709
+ // for a ratio caption, as a percentage.
710
+ if ((path === 'count' || (path === 'ratio' && this.isCaptionRatio)) && counted) {
711
+ return this.isCaptionRatio ? this.formatCaptionRatio(count) : String(count)
700
712
  }
701
713
  // `{value}` is the tile's own number, so a caption can read
702
714
  // "{value} due today" on a register-counted tile that has no
@@ -709,6 +721,42 @@ export default {
709
721
  }).replace(/\s{2,}/g, ' ').trim()
710
722
  },
711
723
 
724
+ /**
725
+ * Whether the caption is a share (`captionSource.kind: "ratio"`).
726
+ *
727
+ * @return {boolean}
728
+ * @spec openspec/changes/stat-caption-ratio/specs/stat-widget/spec.md#requirement-a-stat-tiles-caption-can-show-a-share
729
+ */
730
+ isCaptionRatio() {
731
+ const own = this.content.captionSource
732
+ return !!own && typeof own === 'object' && own.kind === 'ratio'
733
+ },
734
+
735
+ /**
736
+ * The caption's own number: its count, or for a ratio caption the
737
+ * numerator (the tile's own value when it names none) over the
738
+ * denominator, times 100. Null while unread, and for a zero or failed
739
+ * denominator.
740
+ *
741
+ * @return {number|null}
742
+ * @spec openspec/changes/stat-caption-ratio/specs/stat-widget/spec.md#requirement-a-stat-tiles-caption-can-show-a-share
743
+ */
744
+ captionCount() {
745
+ if (!this.isCaptionRatio) {
746
+ return this.captionValue
747
+ }
748
+ const parts = this.captionRatioParts
749
+ if (!parts) {
750
+ return null
751
+ }
752
+ const num = parts.num === null ? this.displayValue : parts.num
753
+ const den = Number(parts.den)
754
+ if (num === null || num === undefined || num === '' || !Number.isFinite(Number(num)) || !Number.isFinite(den) || den === 0) {
755
+ return null
756
+ }
757
+ return (Number(num) / den) * 100
758
+ },
759
+
712
760
  /**
713
761
  * Whether the caption has a count of its own (`content.captionSource`
714
762
  * with a register and schema, its own or the tile's).
@@ -737,7 +785,7 @@ export default {
737
785
  return null
738
786
  }
739
787
  // A caption with its own count is coloured by that count.
740
- const current = this.hasCaptionSource ? this.captionValue : this.displayValue
788
+ const current = this.hasCaptionSource ? this.captionCount : this.displayValue
741
789
  if (current === null || current === undefined) {
742
790
  return null
743
791
  }
@@ -1447,7 +1495,16 @@ export default {
1447
1495
  /** Inline style for the value text (variant rule wins over valueColor). */
1448
1496
  valueStyle() {
1449
1497
  const color = this.variantColor || this.content.valueColor
1450
- return color ? { color } : {}
1498
+ const style = color ? { color } : {}
1499
+ if (this.fitFontSize !== null) {
1500
+ style.fontSize = `${this.fitFontSize}px`
1501
+ // At the smallest size a number that still does not fit wraps rather than clips.
1502
+ if (this.fitFontSize <= MIN_FIT_FONT_SIZE) {
1503
+ style.whiteSpace = 'normal'
1504
+ style.overflowWrap = 'anywhere'
1505
+ }
1506
+ }
1507
+ return style
1451
1508
  },
1452
1509
 
1453
1510
  /**
@@ -1556,6 +1613,10 @@ export default {
1556
1613
  this.fetchValue()
1557
1614
  },
1558
1615
 
1616
+ shownValue() {
1617
+ this.$nextTick(() => this.fitValue())
1618
+ },
1619
+
1559
1620
  objectFieldRaw: {
1560
1621
  immediate: true,
1561
1622
  handler() {
@@ -1576,13 +1637,50 @@ export default {
1576
1637
  payload?.waitUntil?.(done)
1577
1638
  }
1578
1639
  subscribe(PAGE_REFRESH_CHANNEL, this._onPageRefresh)
1640
+ if (typeof ResizeObserver === 'function' && this.$refs.valueRow) {
1641
+ this._fitObserver = new ResizeObserver(() => this.fitValue())
1642
+ this._fitObserver.observe(this.$refs.valueRow)
1643
+ }
1644
+ this.$nextTick(() => this.fitValue())
1579
1645
  },
1580
1646
 
1581
1647
  beforeUnmount() {
1582
1648
  unsubscribe(PAGE_REFRESH_CHANNEL, this._onPageRefresh)
1649
+ this._fitObserver?.disconnect()
1583
1650
  },
1584
1651
 
1585
1652
  methods: {
1653
+ /**
1654
+ * Step the number's font down until it fits its row, instead of the
1655
+ * card clipping it ("€ 1.012.4" in a narrow tile). A value that fits
1656
+ * keeps its own size; text values wrap and clamp on their own.
1657
+ *
1658
+ * @return {void}
1659
+ * @spec openspec/changes/stat-value-fits/specs/stat-tile-board-look/spec.md#requirement-a-stat-value-shrinks-instead-of-clipping
1660
+ */
1661
+ fitValue() {
1662
+ const row = this.$refs.valueRow
1663
+ const el = this.$refs.value
1664
+ if (!row || !el || el.classList.contains('cn-kpi-card__value--text') || typeof getComputedStyle !== 'function') {
1665
+ this.fitFontSize = null
1666
+ return
1667
+ }
1668
+ const applied = el.style.fontSize
1669
+ const whiteSpace = el.style.whiteSpace
1670
+ el.style.fontSize = ''
1671
+ el.style.whiteSpace = ''
1672
+ const natural = parseFloat(getComputedStyle(el).fontSize)
1673
+ const overflow = row.scrollWidth - row.clientWidth
1674
+ const width = el.offsetWidth
1675
+ el.style.fontSize = applied
1676
+ el.style.whiteSpace = whiteSpace
1677
+ if (!(overflow > 0.5) || !(width > 0) || !(natural > 0)) {
1678
+ this.fitFontSize = null
1679
+ return
1680
+ }
1681
+ this.fitFontSize = Math.max(MIN_FIT_FONT_SIZE, Math.floor(natural * ((width - overflow) / width) * 10) / 10)
1682
+ },
1683
+
1586
1684
  /**
1587
1685
  * Resolve an `objectField` that holds a reference uuid to the referenced
1588
1686
  * object's label, through the shared object store (per-schema caching and
@@ -1843,7 +1941,9 @@ export default {
1843
1941
  /**
1844
1942
  * Read the caption's own count (`content.captionSource`:
1845
1943
  * `{ register?, schema?, metric?, field?, filter?, via?, zeroText? }`,
1846
- * register and schema default to the tile's source). A failed read
1944
+ * register and schema default to the tile's source). With `kind:
1945
+ * "ratio"` it reads the `denominator` part, and the `numerator` part
1946
+ * when one is named, as the tile's `ratio` source does. A failed read
1847
1947
  * leaves the caption out; it never shows a zero it did not count.
1848
1948
  *
1849
1949
  * @spec openspec/changes/screens-stat-caption-source/specs/stat-widget/spec.md#requirement-a-stat-tiles-caption-can-carry-its-own-count
@@ -1852,6 +1952,7 @@ export default {
1852
1952
  async fetchCaptionValue() {
1853
1953
  if (!this.hasCaptionSource) {
1854
1954
  this.captionValue = null
1955
+ this.captionRatioParts = null
1855
1956
  return
1856
1957
  }
1857
1958
  const own = this.content.captionSource
@@ -1862,13 +1963,37 @@ export default {
1862
1963
  import('@nextcloud/axios'),
1863
1964
  import('@nextcloud/router'),
1864
1965
  ])
1966
+ if (own.kind === 'ratio') {
1967
+ // The parts of the tile's own `ratio` source: each names its
1968
+ // metric, field, filter and `via`.
1969
+ const part = (p) => this.fetchAggregate(axios, generateUrl, source, (p && p.metric) || own.metric, (p && p.field) || own.field, (p && p.filter) || {}, (p && p.via) || own.via)
1970
+ const num = own.numerator ? await part(own.numerator) : null
1971
+ const den = await part(own.denominator)
1972
+ this.captionRatioParts = { num: num === undefined ? null : num, den }
1973
+ return
1974
+ }
1865
1975
  const found = await this.fetchAggregate(axios, generateUrl, source, own.metric, own.field, own.filter || {}, own.via)
1866
1976
  this.captionValue = found === null || found === undefined || Number.isNaN(Number(found)) ? null : Number(found)
1867
1977
  } catch {
1868
1978
  this.captionValue = null
1979
+ this.captionRatioParts = null
1869
1980
  }
1870
1981
  },
1871
1982
 
1983
+ /**
1984
+ * A ratio caption's share as the reader's number, with
1985
+ * `captionSource.decimals` fraction digits (0 by default).
1986
+ *
1987
+ * @param {number} share The percentage.
1988
+ * @return {string}
1989
+ * @spec openspec/changes/stat-caption-ratio/specs/stat-widget/spec.md#requirement-a-stat-tiles-caption-can-show-a-share
1990
+ */
1991
+ formatCaptionRatio(share) {
1992
+ const raw = Number(this.content.captionSource && this.content.captionSource.decimals)
1993
+ const digits = Number.isInteger(raw) && raw >= 0 && raw <= 4 ? raw : 0
1994
+ return new Intl.NumberFormat(metricLocale(), { minimumFractionDigits: digits, maximumFractionDigits: digits }).format(share)
1995
+ },
1996
+
1872
1997
  /**
1873
1998
  * Resolve the widget's value from its `source`. Supports three source
1874
1999
  * kinds (ADR-041): a plain `aggregate` (count/sum/avg/min/max with
@@ -941,11 +941,14 @@ export default {
941
941
  const from = tab && typeof tab === 'object' ? tab.countFrom : null
942
942
  let source = null
943
943
  if (from === 'widget') {
944
- // A stack counts its first widget that has a list.
944
+ // A stack counts its first widget that has a list: flat
945
+ // (`content.register`) or a dashboard table's `content.source`.
945
946
  const lists = this.tabWidgetIds(tab)
946
947
  .map((id) => this.availableWidgets.find((w) => w && w.id === id))
947
- .filter((w) => w && w.content && typeof w.content === 'object' && w.content.register && w.content.schema)
948
- source = lists.length ? lists[0].content : null
948
+ .map((w) => (w && w.content && typeof w.content === 'object' ? w.content : null))
949
+ .map((content) => (content && content.source && typeof content.source === 'object' && content.source.register ? content.source : content))
950
+ .filter((content) => content && content.register && content.schema)
951
+ source = lists.length ? lists[0] : null
949
952
  } else if (from && typeof from === 'object') {
950
953
  source = from
951
954
  }
@@ -79,6 +79,13 @@ import { serverErrorMessage } from '../../utils/serverErrorMessage.js'
79
79
  * the keys of the rows that are in it; the switch is on when the row's key is
80
80
  * in the list, or NOT in it with `bind.invert` (a list of hidden items shown
81
81
  * as "visible" switches).
82
+ * - `bind.mode: 'each'` — every row IS an object (read by `source`, its key the
83
+ * object id); the switch is that object's own boolean field (`bind.field`)
84
+ * and a toggle PATCHes that object (`bind.register` / `bind.schema`, else the
85
+ * surface's). No current object is needed, so it serves a settings page.
86
+ * - `bind.mode: 'map'` — a config map holds a boolean per row key: read from
87
+ * `bind.url` (at `bind.path`) and written through `save`, or one object field
88
+ * of the current object (`bind.field`) written by its dot-path.
82
89
  *
83
90
  * A toggle saves at once with ONE PATCH of that field; a refused PATCH puts the
84
91
  * switch back and shows the server's words.
@@ -98,7 +105,7 @@ export default {
98
105
  /**
99
106
  * The widget configuration.
100
107
  *
101
- * - `bind` — `{ mode: 'flag' }` (default) or `{ mode: 'list', field, invert? }`.
108
+ * - `bind` — `{ mode: 'flag' }` (default), `{ mode: 'list', field, invert? }`, `{ mode: 'each', field, register?, schema?, invert? }` or `{ mode: 'map', url?, path?, field?, invert? }`.
102
109
  * - `rows` — static rows `[{ key, label, hint?, field?, ...extra }]`.
103
110
  * - `source` — `{ url, path?, key, label, hint? }`: rows read from an endpoint (tokens `@objectId`, `@object.<field>`), in addition to `rows`.
104
111
  * - `columns` — extra columns `[{ header, path, map? }]` read off each row.
@@ -149,13 +156,33 @@ export default {
149
156
  notice: '',
150
157
  /** Per row, how often a refused toggle forced the switch to be drawn again from the stored value. */
151
158
  undone: {},
159
+ /** `each` and `map`: the value written per row key. */
160
+ rowSaved: {},
161
+ /** `map` with `bind.url`: the map as read. */
162
+ map: null,
152
163
  }
153
164
  },
154
165
 
155
166
  computed: {
167
+ /**
168
+ * How a switch is stored.
169
+ *
170
+ * @return {object} The normalised `bind`.
171
+ * @spec openspec/changes/toggle-table-objects-and-map/specs/toggle-table/spec.md#requirement-a-toggle-table-can-switch-objects-or-a-config-map
172
+ */
156
173
  bind() {
157
174
  const bind = this.content.bind || {}
158
- return bind.mode === 'list' ? { mode: 'list', field: bind.field, invert: bind.invert === true } : { mode: 'flag' }
175
+ const invert = bind.invert === true
176
+ if (bind.mode === 'list') {
177
+ return { mode: 'list', field: bind.field, invert }
178
+ }
179
+ if (bind.mode === 'each') {
180
+ return { mode: 'each', field: bind.field || '', invert, register: bind.register || this.register, schema: bind.schema || this.schema }
181
+ }
182
+ if (bind.mode === 'map') {
183
+ return { mode: 'map', field: bind.field || '', url: bind.url || '', path: bind.path || '', invert }
184
+ }
185
+ return { mode: 'flag' }
159
186
  },
160
187
 
161
188
  /** The drawn rows: configured first, then sourced, each with a key, title and hint. */
@@ -200,12 +227,15 @@ export default {
200
227
  watch: {
201
228
  objectData() {
202
229
  this.saved = {}
230
+ this.rowSaved = {}
203
231
  this.loadSource()
232
+ this.loadMap()
204
233
  },
205
234
  },
206
235
 
207
236
  created() {
208
237
  this.loadSource()
238
+ this.loadMap()
209
239
  },
210
240
 
211
241
  methods: {
@@ -233,6 +263,10 @@ export default {
233
263
  * @return {boolean}
234
264
  */
235
265
  isOn(row) {
266
+ if (this.bind.mode === 'each' || this.bind.mode === 'map') {
267
+ const on = this.rowValue(row) === true
268
+ return this.bind.invert ? !on : on
269
+ }
236
270
  if (this.bind.mode === 'list') {
237
271
  const list = this.stored(this.bind.field)
238
272
  const inList = Array.isArray(list) && list.map(String).includes(row.key)
@@ -241,6 +275,92 @@ export default {
241
275
  return row.field ? this.stored(row.field) === true : false
242
276
  },
243
277
 
278
+ /**
279
+ * `each` and `map`: the stored value of a row's switch, as last written,
280
+ * else read off the row's object or the map.
281
+ *
282
+ * @param {object} row The row.
283
+ * @return {unknown}
284
+ * @spec openspec/changes/toggle-table-objects-and-map/specs/toggle-table/spec.md#requirement-a-toggle-table-can-switch-objects-or-a-config-map
285
+ */
286
+ rowValue(row) {
287
+ if (Object.hasOwn(this.rowSaved, row.key)) {
288
+ return this.rowSaved[row.key]
289
+ }
290
+ if (this.bind.mode === 'each') {
291
+ return this.bind.field ? readPath(row.item || row, this.bind.field) : undefined
292
+ }
293
+ const map = this.bind.url ? this.map : (this.bind.field ? this.stored(this.bind.field) : null)
294
+ return map && typeof map === 'object' ? map[row.key] : undefined
295
+ },
296
+
297
+ /**
298
+ * Resolve `@objectId` and `@object.<path>` in a url.
299
+ *
300
+ * @param {string} url The url.
301
+ * @return {string}
302
+ */
303
+ fillUrl(url) {
304
+ const object = this.objectData || {}
305
+ return String(url)
306
+ .replace(/@objectId/g, () => encodeURIComponent(String(this.objectId || object.id || '')))
307
+ .replace(/@object\.([\w.@]+)/g, (m, path) => encodeURIComponent(String(readPath(object, path) ?? '')))
308
+ },
309
+
310
+ /**
311
+ * `map` with `bind.url`: read the map.
312
+ *
313
+ * @return {Promise<void>}
314
+ * @spec openspec/changes/toggle-table-objects-and-map/specs/toggle-table/spec.md#requirement-a-toggle-table-can-switch-objects-or-a-config-map
315
+ */
316
+ async loadMap() {
317
+ if (this.bind.mode !== 'map' || !this.bind.url) {
318
+ return
319
+ }
320
+ try {
321
+ const { default: axios } = await import('@nextcloud/axios')
322
+ const { data } = await axios.get(prefixUrl(this.fillUrl(this.bind.url)))
323
+ const map = this.bind.path ? readPath(data, this.bind.path) : data
324
+ this.map = map && typeof map === 'object' ? map : {}
325
+ } catch (e) {
326
+ this.notice = serverErrorMessage(e)
327
+ }
328
+ },
329
+
330
+ /**
331
+ * `each` and `map`: write one row's switch.
332
+ *
333
+ * @param {object} row The row.
334
+ * @param {boolean} value The value to store.
335
+ * @return {Promise<unknown>} The value the server holds now.
336
+ * @spec openspec/changes/toggle-table-objects-and-map/specs/toggle-table/spec.md#requirement-a-toggle-table-can-switch-objects-or-a-config-map
337
+ */
338
+ async writeRow(row, value) {
339
+ const save = this.content.save
340
+ const hasSave = Boolean(save && save.url)
341
+ if (this.bind.mode === 'each') {
342
+ const url = objectUrlOf(this.bind.register, this.bind.schema, row.key)
343
+ if ((!url && !hasSave) || !this.bind.field) {
344
+ throw new Error(this.tr('This switch cannot be saved here.'))
345
+ }
346
+ const data = await writeFields(url, { [this.bind.field]: value }, { save, object: row.item || row })
347
+ const answered = hasSave ? undefined : readPath(data, this.bind.field)
348
+ return answered !== undefined ? answered : value
349
+ }
350
+ if (this.bind.url) {
351
+ if (!hasSave) {
352
+ throw new Error(this.tr('This switch cannot be saved here.'))
353
+ }
354
+ await writeFields('', { [row.key]: value }, { save, object: this.objectData })
355
+ return value
356
+ }
357
+ if ((!this.objectUrl && !hasSave) || !this.bind.field) {
358
+ throw new Error(this.tr('This switch cannot be saved here.'))
359
+ }
360
+ await writeFields(this.objectUrl, { [`${this.bind.field}.${row.key}`]: value }, { save, object: this.objectData })
361
+ return value
362
+ },
363
+
244
364
  /**
245
365
  * The text of an extra column for a row.
246
366
  *
@@ -265,10 +385,7 @@ export default {
265
385
  if (!source || !source.url || !source.key) {
266
386
  return
267
387
  }
268
- const object = this.objectData || {}
269
- const url = String(source.url)
270
- .replace(/@objectId/g, () => encodeURIComponent(String(this.objectId || object.id || '')))
271
- .replace(/@object\.([\w.@]+)/g, (m, path) => encodeURIComponent(String(readPath(object, path) ?? '')))
388
+ const url = this.fillUrl(source.url)
272
389
  try {
273
390
  const { default: axios } = await import('@nextcloud/axios')
274
391
  const { data } = await axios.get(prefixUrl(url))
@@ -277,6 +394,7 @@ export default {
277
394
  .filter((item) => item && readPath(item, source.key) !== undefined)
278
395
  .map((item) => ({
279
396
  ...item,
397
+ item,
280
398
  key: String(readPath(item, source.key)),
281
399
  title: String(readPath(item, source.label || source.key) ?? readPath(item, source.key)),
282
400
  hint: source.hint ? String(readPath(item, source.hint) ?? '') : '',
@@ -295,6 +413,21 @@ export default {
295
413
  * @spec openspec/changes/abstract-toggle-table/specs/toggle-table/spec.md#requirement-a-toggle-saves-one-field
296
414
  */
297
415
  async toggle(row, on) {
416
+ if (this.bind.mode === 'each' || this.bind.mode === 'map') {
417
+ this.notice = ''
418
+ this.busy = row.key
419
+ try {
420
+ const written = await this.writeRow(row, this.bind.invert ? !on : on)
421
+ this.rowSaved = { ...this.rowSaved, [row.key]: written }
422
+ this.$emit('saved', { field: this.bind.mode === 'each' ? this.bind.field : row.key, key: row.key, value: written })
423
+ } catch (e) {
424
+ this.notice = serverErrorMessage(e)
425
+ this.undone = { ...this.undone, [row.key]: (this.undone[row.key] || 0) + 1 }
426
+ } finally {
427
+ this.busy = ''
428
+ }
429
+ return
430
+ }
298
431
  const field = this.bind.mode === 'list' ? this.bind.field : row.field
299
432
  if ((!this.objectUrl && !(this.content.save && this.content.save.url)) || !field) {
300
433
  this.notice = this.tr('This switch cannot be saved here.')
@@ -317,7 +450,7 @@ export default {
317
450
  this.saved = { ...this.saved, [field]: written }
318
451
  /**
319
452
  * @event saved A switch was written.
320
- * @type {{field: string, value: unknown}}
453
+ * @type {{field: string, key?: string, value: unknown}} `key` is the row, in the `each` and `map` modes.
321
454
  */
322
455
  this.$emit('saved', { field, value: written })
323
456
  } catch (e) {
@@ -87,9 +87,10 @@ With `title`, `description`, `docUrl`, `cardTitle`, `loading`, and `labels`:
87
87
  | `title` | String | `'Version information'` | Section heading (passed to the wrapping CnSettingsSection) |
88
88
  | `description` | String | `'Information about the current application installation'` | Section sub-heading |
89
89
  | `docUrl` | String | `''` | Documentation URL — shows an info icon link next to the section title |
90
+ | `wide` | Boolean | `true` | Under the board look the card spans the whole settings grid, as the boards draw it. `false` keeps it one card in the grid. No effect without the board look. |
90
91
  | `cardTitle` | String | `'Application information'` | Heading inside the gray version card |
91
92
  | `loading` | Boolean | `false` | Show a loading spinner and hide the card content while data is fetching |
92
- | `labels` | Object | `{ appName, version, configuredVersion }` | Custom labels for the standard row headings (for i18n) |
93
+ | `labels` | Object | `{}` | Custom labels for the standard row headings (`appName`, `version`, `configuredVersion`); a label left out keeps the translated default |
93
94
 
94
95
 
95
96
  `look` is documented in the prop table of the component reference (board look, screens-chrome-parity).
@@ -4,7 +4,8 @@
4
4
  :description="description"
5
5
  :docUrl="docUrl"
6
6
  :loading="loading"
7
- loadingMessage="Loading version information...">
7
+ :wide="isBoard && wide"
8
+ :loadingMessage="t('nextcloud-vue', 'Loading version information…')">
8
9
  <!-- Actions slot (top right). Under the board look the same buttons sit
9
10
  in the footer row of the card instead. -->
10
11
  <template v-if="!isBoard" #actions>
@@ -34,19 +35,19 @@
34
35
  <div class="cn-version-info__details">
35
36
  <!-- Application Name -->
36
37
  <div class="cn-version-info__item">
37
- <span class="cn-version-info__label">{{ labels.appName }}:</span>
38
+ <span class="cn-version-info__label">{{ fieldLabels.appName }}:</span>
38
39
  <span class="cn-version-info__value">{{ appName }}</span>
39
40
  </div>
40
41
 
41
42
  <!-- Version -->
42
43
  <div class="cn-version-info__item">
43
- <span class="cn-version-info__label">{{ labels.version }}:</span>
44
+ <span class="cn-version-info__label">{{ fieldLabels.version }}:</span>
44
45
  <span class="cn-version-info__value">{{ appVersion }}</span>
45
46
  </div>
46
47
 
47
48
  <!-- Configured Version (if provided) -->
48
49
  <div v-if="configuredVersion" class="cn-version-info__item">
49
- <span class="cn-version-info__label">{{ labels.configuredVersion }}:</span>
50
+ <span class="cn-version-info__label">{{ fieldLabels.configuredVersion }}:</span>
50
51
  <span class="cn-version-info__value">{{ configuredVersion }}</span>
51
52
  </div>
52
53
 
@@ -157,6 +158,18 @@ export default {
157
158
  default: '',
158
159
  },
159
160
 
161
+ /**
162
+ * Under the board look the card spans the whole settings grid, as the
163
+ * boards draw the version card. Set to `false` to keep it one card in
164
+ * the grid. Without the board look it has no effect.
165
+ *
166
+ * @spec openspec/changes/board-version-card-full-width/specs/settings-board-look/spec.md#requirement-the-board-version-card-spans-the-settings-grid
167
+ */
168
+ wide: {
169
+ type: Boolean,
170
+ default: true,
171
+ },
172
+
160
173
  /** Section title */
161
174
  title: {
162
175
  type: String,
@@ -232,14 +245,13 @@ export default {
232
245
  default: false,
233
246
  },
234
247
 
235
- /** Custom labels for the standard fields */
248
+ /**
249
+ * Custom labels for the standard fields, `{ appName?, version?, configuredVersion? }`.
250
+ * A label left out keeps the translated default.
251
+ */
236
252
  labels: {
237
253
  type: Object,
238
- default: () => ({
239
- appName: 'Application Name',
240
- version: 'Version',
241
- configuredVersion: 'Configured Version',
242
- }),
254
+ default: () => ({}),
243
255
  },
244
256
  },
245
257
 
@@ -251,6 +263,21 @@ export default {
251
263
  },
252
264
 
253
265
  computed: {
266
+ /**
267
+ * The field labels: the app's, else the library's in the reader's language.
268
+ *
269
+ * @return {{appName: string, version: string, configuredVersion: string}}
270
+ * @spec openspec/changes/version-card-dutch-and-header/specs/settings-board-look/spec.md#requirement-the-admin-chrome-reads-in-the-readers-language
271
+ */
272
+ fieldLabels() {
273
+ return {
274
+ appName: t('nextcloud-vue', 'Application name'),
275
+ version: t('nextcloud-vue', 'Version'),
276
+ configuredVersion: t('nextcloud-vue', 'Configured version'),
277
+ ...(this.labels || {}),
278
+ }
279
+ },
280
+
254
281
  /** @return {string} Button type based on update status */
255
282
  updateButtonType() {
256
283
  if (this.isUpToDate) {
@@ -267,16 +294,18 @@ export default {
267
294
  /** @return {string} Update button label text */
268
295
  updateButtonText() {
269
296
  if (this.updating) {
270
- return 'Updating...'
297
+ return t('nextcloud-vue', 'Updating…')
271
298
  }
272
299
  if (this.isUpToDate) {
273
- return 'Up to date'
300
+ return t('nextcloud-vue', 'Up to date')
274
301
  }
275
- return 'Update'
302
+ return t('nextcloud-vue', 'Update')
276
303
  },
277
304
  },
278
305
 
279
306
  methods: {
307
+ t,
308
+
280
309
  handleUpdateClick() {
281
310
  if (!this.updateButtonDisabled) {
282
311
  this.$emit('update')
@@ -89,7 +89,8 @@ const DEFAULT_LIMIT = 100
89
89
  *
90
90
  * It renders the five working days by default (`days: 7` adds the weekend),
91
91
  * highlights today, marks items that are late, and says so when a day has
92
- * nothing. On a narrow screen the strip scrolls sideways.
92
+ * nothing. On a narrow screen the strip scrolls sideways; under the board
93
+ * look (`.cn-look-board`) the days wrap into rows instead.
93
94
  *
94
95
  * Items come from an OpenRegister source (one request for the whole week) or
95
96
  * from static `items`. Resolved by its registry type key `week-strip`.