@conduction/nextcloud-vue 2.138.0 → 2.140.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 (115) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +9 -2
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  4. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +17 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +2 -2
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  10. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +52 -1
  11. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  12. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +23 -1
  13. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  14. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
  15. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +106 -0
  16. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  17. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +30 -7
  18. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  19. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  20. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +59 -3
  21. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  22. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +44 -34
  23. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  24. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  25. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +28 -1
  26. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  28. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +7 -1
  29. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  30. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +5 -0
  31. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +129 -6
  33. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +2 -0
  37. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  38. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +13 -2
  39. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  40. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  41. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +28 -3
  42. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  43. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  44. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  45. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
  46. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  47. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  48. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +1 -1
  49. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +10 -0
  50. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -1
  51. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -1
  52. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +1 -1
  53. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +12 -1
  54. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -1
  55. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +1 -1
  56. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue.js +1 -1
  58. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js +23 -1
  59. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js.map +1 -1
  60. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js +43 -13
  61. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js.map +1 -1
  62. package/dist/esm/composables/indexSources.js +48 -6
  63. package/dist/esm/composables/indexSources.js.map +1 -1
  64. package/dist/esm/composables/useTaskInboxStore.js +4 -3
  65. package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
  66. package/dist/esm/l10n/en.json.js +4 -1
  67. package/dist/esm/l10n/en.json.js.map +1 -1
  68. package/dist/esm/l10n/nl.json.js +7 -1
  69. package/dist/esm/l10n/nl.json.js.map +1 -1
  70. package/dist/esm/library-style.js +1 -1
  71. package/dist/esm/nextcloud-vue.css +604 -407
  72. package/dist/esm/utils/builtInFormatters.js +23 -1
  73. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  74. package/dist/esm/utils/fillTemplate.js +7 -1
  75. package/dist/esm/utils/fillTemplate.js.map +1 -1
  76. package/dist/esm/utils/myTeam.js +24 -4
  77. package/dist/esm/utils/myTeam.js.map +1 -1
  78. package/dist/esm/utils/resolveFilterTokens.js +5 -4
  79. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  80. package/dist/esm/utils/transitionInputs.js +43 -2
  81. package/dist/esm/utils/transitionInputs.js.map +1 -1
  82. package/dist/nextcloud-vue.cjs.js +932 -236
  83. package/dist/nextcloud-vue.cjs.js.map +2 -2
  84. package/dist/nextcloud-vue.css +604 -407
  85. package/l10n/en.json +4 -1
  86. package/l10n/nl.json +7 -1
  87. package/package.json +1 -1
  88. package/src/components/CnAppRoot/CnAppRoot.vue +9 -2
  89. package/src/components/CnCellRenderer/CnCellRenderer.vue +19 -3
  90. package/src/components/CnChartWidget/CnChartWidget.md +1 -0
  91. package/src/components/CnChartWidget/CnChartWidget.vue +76 -1
  92. package/src/components/CnConversationThread/CnConversationWidget.vue +126 -1
  93. package/src/components/CnFactsWidget/CnFactsWidget.vue +78 -9
  94. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +39 -1
  95. package/src/components/CnFilesBrowser/filesBrowser.js +7 -1
  96. package/src/components/CnFilesBrowser/filesBrowserColumns.js +5 -0
  97. package/src/components/CnIndexPage/CnIndexPage.vue +131 -8
  98. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +10 -2
  99. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +28 -3
  100. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
  101. package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +10 -0
  102. package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +68 -2
  103. package/src/components/CnTransitionInputFields/CnTransitionInputFields.vue +43 -1
  104. package/src/composables/indexSources.js +48 -6
  105. package/src/composables/useTaskInboxStore.js +4 -3
  106. package/src/css/dialog.css +18 -0
  107. package/src/css/look-board-detail.css +24 -0
  108. package/src/css/look-board-index.css +21 -0
  109. package/src/css/look-board.css +31 -2
  110. package/src/css/table.css +13 -0
  111. package/src/utils/builtInFormatters.js +22 -0
  112. package/src/utils/fillTemplate.js +7 -1
  113. package/src/utils/myTeam.js +24 -4
  114. package/src/utils/resolveFilterTokens.js +5 -4
  115. package/src/utils/transitionInputs.js +42 -1
@@ -775,6 +775,7 @@ import CnIcon from '../CnIcon/CnIcon.vue'
775
775
  import { readUserPreference, writeUserPreference } from '../../composables/useUserPreferences.js'
776
776
  import { dispatchAction } from '../../utils/actionsDispatcher.js'
777
777
  import { fillTemplate } from '../../utils/fillTemplate.js'
778
+ import { readPath } from '../../utils/readPath.js'
778
779
  import { isNewTabClick, isRowMiddleClick, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
779
780
  import { ACTIONS_NEEDING_THE_FILES_PAGE, crumbsFor, hostActionApplies, joinPath } from './filesBrowser.js'
780
781
  import {
@@ -3049,7 +3050,7 @@ export default {
3049
3050
  ? { path: this.currentPath }
3050
3051
  : { fileId: node.fileid, fileName: node.basename, path: node.path }
3051
3052
  if (type === 'open-modal') {
3052
- wrapped = { ...action, props: { ...(action.props || {}), ...context } }
3053
+ wrapped = { ...action, props: { ...(action.props || {}), ...context, ...this.rowPropsOf(action, node) } }
3053
3054
  } else if (type === 'handler') {
3054
3055
  wrapped = { ...action, args: [...(action.args || []), node ?? this.folder] }
3055
3056
  }
@@ -3060,6 +3061,32 @@ export default {
3060
3061
  dispatchAction(wrapped, { router: this.$router || null })
3061
3062
  },
3062
3063
 
3064
+ /**
3065
+ * The clicked file's row-data fields an action asks for as modal props
3066
+ * (`rowProps: { prop: field }`), or none.
3067
+ *
3068
+ * @param {object} action The declared action.
3069
+ * @param {object|null} node The file, or null for a New-menu entry.
3070
+ * @return {object} The props.
3071
+ * @spec openspec/changes/files-review-row-when/specs/files-review-row-when/spec.md#requirement-a-modal-action-can-carry-fields-of-the-clicked-files-row-data
3072
+ */
3073
+ rowPropsOf(action, node) {
3074
+ const wanted = action && action.rowProps
3075
+ if (node === null || !wanted || typeof wanted !== 'object') {
3076
+ return {}
3077
+ }
3078
+ const byId = this.resolvedRowData && typeof this.resolvedRowData === 'object' ? this.resolvedRowData : {}
3079
+ const entry = byId[node.fileid] ?? byId[String(node.fileid)] ?? {}
3080
+ const out = {}
3081
+ for (const [prop, field] of Object.entries(wanted)) {
3082
+ const value = readPath(entry, String(field))
3083
+ if (value !== undefined && value !== null && value !== '') {
3084
+ out[prop] = value
3085
+ }
3086
+ }
3087
+ return out
3088
+ },
3089
+
3063
3090
  /**
3064
3091
  * A click on a linked row opens the file where it lives.
3065
3092
  *
@@ -3278,6 +3305,17 @@ export default {
3278
3305
  font-size: 14px;
3279
3306
  }
3280
3307
 
3308
+ /* Core's apps.css draws every dt/dd inline-block with a fixed, end-aligned dt.
3309
+ @spec openspec/changes/files-side-facts-core-dl/specs/files-side-facts-core-dl/spec.md#requirement-a-file-side-cards-facts-are-not-drawn-by-cores-dl-rules */
3310
+ .cn-files-browser__side-facts dt,
3311
+ .cn-files-browser__side-facts dd {
3312
+ display: block;
3313
+ width: auto;
3314
+ padding: 0;
3315
+ text-align: start;
3316
+ white-space: normal;
3317
+ }
3318
+
3281
3319
  .cn-files-browser__side-facts dt {
3282
3320
  color: var(--color-text-maxcontrast);
3283
3321
  }
@@ -285,13 +285,19 @@ export function indexRowData(data, { path = '', key = 'fileId' } = {}) {
285
285
  * `visibleIf.extension` lists file extensions (no case, a leading dot is
286
286
  * allowed); `visibleIf.mime` lists mime types, where one ending in `/` or
287
287
  * `/*` names the whole family. Every condition named must hold. An action
288
- * without `visibleIf` is offered on every file, as before.
288
+ * without `visibleIf` is offered on every file, as before. An action with
289
+ * `when` is offered only while `when` is `true`.
289
290
  *
290
291
  * @param {object} action The declared action.
291
292
  * @param {object} node The file (`basename`, `mime`).
292
293
  * @return {boolean} True when the action belongs on this file.
293
294
  */
294
295
  export function hostActionApplies(action, node) {
296
+ // `when`: the action applies only while it is true (a case type flag).
297
+ // @spec openspec/changes/files-review-row-when/specs/files-review-row-when/spec.md#requirement-a-files-browser-entry-can-apply-only-when-a-condition-holds
298
+ if (action && typeof action === 'object' && 'when' in action && action.when !== true) {
299
+ return false
300
+ }
295
301
  const condition = action && action.visibleIf
296
302
  if (!condition || typeof condition !== 'object') {
297
303
  return true
@@ -78,6 +78,11 @@ export function normaliseFileColumns(columns) {
78
78
  if (!entry || typeof entry !== 'object' || typeof entry.key !== 'string' || entry.key === '') {
79
79
  return
80
80
  }
81
+ // `when`: the column applies only while it is true (a case type flag).
82
+ // @spec openspec/changes/files-review-row-when/specs/files-review-row-when/spec.md#requirement-a-files-browser-entry-can-apply-only-when-a-condition-holds
83
+ if ('when' in entry && entry.when !== true) {
84
+ return
85
+ }
81
86
  if (isBuiltInFileColumn(entry.key) && entry.source === undefined) {
82
87
  out.push({ ...BUILT_IN_FILE_COLUMNS[entry.key], ...entry, builtIn: true })
83
88
  return
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="cn-index-page"
3
- :class="[lookClass, { 'cn-index-page--with-side-panel': hasSidePanel, 'cn-index-page--quick-filters-toolbar': effectiveQuickFilterPlacement === 'toolbar' }]"
3
+ :class="[lookClass, { 'cn-index-page--with-side-panel': hasSidePanel, 'cn-index-page--sidebar-open': sidebarPanelOpen, 'cn-index-page--quick-filters-toolbar': effectiveQuickFilterPlacement === 'toolbar' }]"
4
4
  data-testid="cn-index-page"
5
5
  @keydown="onListKeydown">
6
6
  <!-- Header — overridable via #header slot. CnPageHeader ALWAYS renders:
@@ -172,7 +172,7 @@
172
172
  :maxVisible="quickFilterMaxVisible"
173
173
  :fit="false"
174
174
  :multiple="quickFilterMultiple"
175
- :activeIndex="activeQuickFilterIndex"
175
+ :activeIndex="shownQuickFilterIndex"
176
176
  :selectedIndices="selectedQuickFilterIndices"
177
177
  :counts="effectiveTabCounts"
178
178
  @update:activeIndex="onQuickFilterChange"
@@ -2240,6 +2240,21 @@ export default {
2240
2240
  default: '',
2241
2241
  },
2242
2242
 
2243
+ /**
2244
+ * Board look: the count line while a saved view is applied (manifest
2245
+ * `config.savedViewCountText`), a template as `countText` that may name
2246
+ * the view's conditions as `{view}`: its description, else its filters
2247
+ * in words (DqFilterInMenu "3 van 48 lopende zaken · termijn binnen 7
2248
+ * dagen, van mij · gesorteerd op termijn"). A view's own `countText`
2249
+ * override and a scope's come first.
2250
+ *
2251
+ * @spec openspec/changes/applied-view-lights-its-lens/specs/applied-view-lights-its-lens/spec.md
2252
+ */
2253
+ savedViewCountText: {
2254
+ type: String,
2255
+ default: '',
2256
+ },
2257
+
2243
2258
  /**
2244
2259
  * Board look: a count line per view (manifest `config.countTextByView`),
2245
2260
  * `{ table?, cards?, list?, board?, map? }`, each a template as
@@ -4580,6 +4595,8 @@ export default {
4580
4595
  savedViewCounts: null,
4581
4596
  // The id (or slug) of the saved view that is applied; its chip shows selected under the board look.
4582
4597
  appliedSavedViewId: '',
4598
+ /** Whether the reader picked a lens chip after the applied view (then their chip shows). */
4599
+ lensChosenByHand: false,
4583
4600
  // The sort option the applied view's `overrides.sortOption` selected, so leaving the view can undo it.
4584
4601
  viewSortOption: '',
4585
4602
  // The applied view's filters as one comparable key, so the address keeps naming the view until they change.
@@ -4653,6 +4670,8 @@ export default {
4653
4670
  savedViews: [],
4654
4671
  savedViewsLoading: false,
4655
4672
  showSaveViewDialog: false,
4673
+ /** Whether the reader changed the view since it was opened or saved. */
4674
+ viewChanged: false,
4656
4675
  viewPendingDelete: null,
4657
4676
  /** An `api-call` header action waiting on its confirmation, or null. */
4658
4677
  headerApiCallPending: null,
@@ -6419,13 +6438,53 @@ export default {
6419
6438
  * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md
6420
6439
  */
6421
6440
  /**
6422
- * The chip the strip lights: the active tab, else the default tab of
6423
- * tabs that arrived after setup (`quickFiltersFrom`), which is the
6424
- * one the fetch applies.
6441
+ * The lens chip an applied saved view narrows within: the non-default
6442
+ * tab whose every condition the view's own conditions (its filters
6443
+ * over its locked filters, tokens as written) hold, the most specific
6444
+ * first (DqFilterInMenu: "Termijn deze week" lit for the view of that
6445
+ * name). Null without an applied view or a fitting tab.
6446
+ *
6447
+ * @return {number|null} The tab index.
6448
+ * @spec openspec/changes/applied-view-lights-its-lens/specs/applied-view-lights-its-lens/spec.md
6449
+ */
6450
+ appliedViewLensIndex() {
6451
+ const view = this.appliedSavedView
6452
+ const tabs = Array.isArray(this.tabStripEntries) ? this.tabStripEntries : []
6453
+ if (!view || tabs.length === 0) {
6454
+ return null
6455
+ }
6456
+ const query = view.query && typeof view.query === 'object' ? view.query : {}
6457
+ const locked = query.overrides && typeof query.overrides.lockedFilters === 'object' ? query.overrides.lockedFilters : {}
6458
+ const conditions = { ...(locked || {}), ...(query.filters || view.filters || {}) }
6459
+ const same = (a, b) => JSON.stringify(Array.isArray(a) ? a.map(String) : String(a)) === JSON.stringify(Array.isArray(b) ? b.map(String) : String(b))
6460
+ let best = null
6461
+ let bestSize = 0
6462
+ tabs.forEach((tab, index) => {
6463
+ const filter = tab && tab.filter && typeof tab.filter === 'object' ? tab.filter : {}
6464
+ const keys = Object.keys(filter)
6465
+ if (tab.default === true || keys.length === 0 || keys.length <= bestSize) {
6466
+ return
6467
+ }
6468
+ if (keys.every((key) => Object.hasOwn(conditions, key) && same(conditions[key], filter[key]))) {
6469
+ best = index
6470
+ bestSize = keys.length
6471
+ }
6472
+ })
6473
+ return best
6474
+ },
6475
+
6476
+ /**
6477
+ * The chip the strip lights: the lens an applied saved view narrows
6478
+ * within (until the reader picks a chip), else the active tab, else
6479
+ * the default tab of tabs that arrived after setup
6480
+ * (`quickFiltersFrom`), which is the one the fetch applies.
6425
6481
  *
6426
6482
  * @return {number|null} The tab index, or null without tabs.
6427
6483
  */
6428
6484
  shownQuickFilterIndex() {
6485
+ if (!this.quickFilterMultiple && !this.lensChosenByHand && this.appliedViewLensIndex !== null) {
6486
+ return this.appliedViewLensIndex
6487
+ }
6429
6488
  if (this.activeQuickFilterIndex !== null && this.activeQuickFilterIndex !== undefined) {
6430
6489
  return this.activeQuickFilterIndex
6431
6490
  }
@@ -8649,12 +8708,20 @@ export default {
8649
8708
 
8650
8709
  /**
8651
8710
  * Whether the toolbar draws Save view: not when the page switches it
8652
- * off (`savedViewSave: false`) or its header carries one.
8711
+ * off (`savedViewSave: false`) or its header carries one, and in the
8712
+ * board look only once the view has changed.
8653
8713
  *
8714
+ * @spec openspec/changes/board-save-view-only-when-changed/specs/saved-views/spec.md#requirement-the-board-look-offers-save-view-once-the-view-has-changed
8654
8715
  * @return {boolean}
8655
8716
  */
8656
8717
  toolbarSavesView() {
8657
- return this.savedViewSave !== false && !this.headerSavesView
8718
+ if (this.savedViewSave === false || this.headerSavesView) {
8719
+ return false
8720
+ }
8721
+ // The board look offers it once the view differs from the one
8722
+ // opened: changed by the reader, or, with no saved view open,
8723
+ // filtered by the address.
8724
+ return !this.isBoardLook || this.viewChanged || (!this.appliedSavedViewId && this.activeFilterChips.length > 0)
8658
8725
  },
8659
8726
 
8660
8727
  /**
@@ -8815,6 +8882,11 @@ export default {
8815
8882
  * @return {string}
8816
8883
  */
8817
8884
  presentedCountText() {
8885
+ const viewLine = this.savedViewCountText
8886
+ const view = this.appliedSavedView
8887
+ if (view && typeof viewLine === 'string' && viewLine !== '' && this.activeScopePresentation.countText === undefined && !savedViewOverrides(view)?.countText) {
8888
+ return viewLine
8889
+ }
8818
8890
  if (this.activeScopePresentation.countText === undefined) {
8819
8891
  const byView = this.countTextByView
8820
8892
  const own = byView && typeof byView === 'object' ? byView[this.currentViewMode] : undefined
@@ -8825,6 +8897,23 @@ export default {
8825
8897
  return this.presented('countText')
8826
8898
  },
8827
8899
 
8900
+ /**
8901
+ * The applied saved view's conditions for the count line's `{view}`:
8902
+ * its description, else its filters in words, starting lower case.
8903
+ *
8904
+ * @return {string}
8905
+ * @spec openspec/changes/applied-view-lights-its-lens/specs/applied-view-lights-its-lens/spec.md
8906
+ */
8907
+ appliedViewConditionsText() {
8908
+ const view = this.appliedSavedView
8909
+ if (!view) {
8910
+ return ''
8911
+ }
8912
+ const own = typeof view.description === 'string' ? view.description.trim() : ''
8913
+ const text = own !== '' ? own : String(this.viewFilterSummary(view) || '')
8914
+ return text.charAt(0).toLowerCase() + text.slice(1)
8915
+ },
8916
+
8828
8917
  headerDescriptionBase() {
8829
8918
  const total = this.effectivePagination?.total
8830
8919
  // Board look: the count line is a template over the rows on this page
@@ -8848,6 +8937,7 @@ export default {
8848
8937
  .replace('{shown}', String(shown))
8849
8938
  .replace('{total}', String(total))
8850
8939
  .replace(/\{term\}/g, this.sourceTerm)
8940
+ .replace(/\{view\}/g, this.appliedViewConditionsText)
8851
8941
  }
8852
8942
  return this.countLine(this.countSubtitle, total, shown)
8853
8943
  }
@@ -8913,6 +9003,17 @@ export default {
8913
9003
  return !!this.resolvedSidebar.enabled && this.resolvedSidebar.show !== false
8914
9004
  },
8915
9005
 
9006
+ /**
9007
+ * Whether the page's side panel is open now; the board look then lays
9008
+ * the header out narrower (DqZakenSorteren).
9009
+ *
9010
+ * @spec openspec/changes/board-header-buttons-under-title-beside-side-panel/specs/board-header-buttons-under-title-beside-side-panel/spec.md#requirement-the-header-buttons-sit-under-the-title-while-the-side-panel-is-open
9011
+ * @return {boolean}
9012
+ */
9013
+ sidebarPanelOpen() {
9014
+ return Boolean(this.sidebarOpen && this.hasSidebar)
9015
+ },
9016
+
8916
9017
  /**
8917
9018
  * Whether the board toolbar draws the Filter button as a menu: the
8918
9019
  * board look, `filterMenu` on, something to filter by, and no filter
@@ -8955,7 +9056,9 @@ export default {
8955
9056
 
8956
9057
  /** Search props forwarded to the embedded CnIndexSidebar (defaults applied per CnIndexSidebar). */
8957
9058
  sidebarSearchProps() {
8958
- return (this.sidebar && this.sidebar.search) || {}
9059
+ // The page's own search hint, unless `sidebar.search` names one.
9060
+ const own = this.shownSearchPlaceholder ? { searchPlaceholder: this.shownSearchPlaceholder } : {}
9061
+ return { ...own, ...((this.sidebar && this.sidebar.search) || {}) }
8959
9062
  },
8960
9063
 
8961
9064
  /**
@@ -10756,6 +10859,7 @@ export default {
10756
10859
  * @return {void}
10757
10860
  */
10758
10861
  onSearchEvent(value) {
10862
+ this.viewChanged = true
10759
10863
  if (this.namedSourceSearches) {
10760
10864
  this.namedSearchTerm = value || ''
10761
10865
  }
@@ -10775,9 +10879,11 @@ export default {
10775
10879
  * @return {void}
10776
10880
  */
10777
10881
  onQuickFilterChange(index) {
10882
+ this.viewChanged = true
10778
10883
  // `activeQuickFilterIndex` is a setup-returned ref; the
10779
10884
  // Vue 2 ref-unwrap proxy makes plain assignment work.
10780
10885
  this.activeQuickFilterIndex = index
10886
+ this.lensChosenByHand = true
10781
10887
  this.$emit('quick-filter-change', index)
10782
10888
  },
10783
10889
 
@@ -10790,6 +10896,7 @@ export default {
10790
10896
  * @return {void}
10791
10897
  */
10792
10898
  onQuickFilterMultiChange(indices) {
10899
+ this.viewChanged = true
10793
10900
  this.selectedQuickFilterIndices = Array.isArray(indices) ? indices : []
10794
10901
  this.$emit('quick-filter-change', this.selectedQuickFilterIndices)
10795
10902
  },
@@ -10805,6 +10912,7 @@ export default {
10805
10912
  * @return {void}
10806
10913
  */
10807
10914
  onSidebarSortChange(keys, value) {
10915
+ this.viewChanged = true
10808
10916
  if (value !== undefined && this.selectSortOption(value)) {
10809
10917
  return
10810
10918
  }
@@ -10884,6 +10992,7 @@ export default {
10884
10992
  * @return {void}
10885
10993
  */
10886
10994
  onSortEvent(payload) {
10995
+ this.viewChanged = true
10887
10996
  // An order by keys (a column header, a field in the Sort section)
10888
10997
  // leaves an endpoint-ranked order.
10889
10998
  if (this.sortSource.value) {
@@ -11087,6 +11196,7 @@ export default {
11087
11196
  * @return {void}
11088
11197
  */
11089
11198
  onClearFilters() {
11199
+ this.viewChanged = true
11090
11200
  // "Clear all" on the Active line also undoes the quick filters it
11091
11201
  // listed (a tab without an `activeLabel` is not a filter there).
11092
11202
  for (const chip of this.quickFilterActiveChips) {
@@ -11195,6 +11305,7 @@ export default {
11195
11305
  * @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-a-board-can-switch-the-field-its-columns-come-from
11196
11306
  */
11197
11307
  onBoardColumnFieldChange(field) {
11308
+ this.viewChanged = true
11198
11309
  this.boardColumnFieldChoice = field
11199
11310
  /**
11200
11311
  * @event board-column-field-change The board's column switch picked another field. Payload: the field name.
@@ -11210,6 +11321,7 @@ export default {
11210
11321
  * @return {void}
11211
11322
  */
11212
11323
  onBoardNarrowChange(value) {
11324
+ this.viewChanged = true
11213
11325
  this.onFilterEvent({ key: this.boardNarrow.field, values: value === '' ? [] : [value] })
11214
11326
  },
11215
11327
 
@@ -11272,6 +11384,7 @@ export default {
11272
11384
  * @return {void}
11273
11385
  */
11274
11386
  onFilterEvent(payload) {
11387
+ this.viewChanged = true
11275
11388
  if (this.isSelfFetchMode && typeof this.list.onFilterChange === 'function') {
11276
11389
  this.list.onFilterChange(payload.key, payload.values)
11277
11390
  this.persistViewStateToRoute(this.currentViewState())
@@ -11294,6 +11407,7 @@ export default {
11294
11407
  * @spec openspec/changes/screens-index-list-parity/specs/index-list-board-look/spec.md#requirement-the-toolbar-sits-on-the-ground-in-two-rows
11295
11408
  */
11296
11409
  onRemoveActiveFilter(chip) {
11410
+ this.viewChanged = true
11297
11411
  if (chip && chip.searchTerm === true) {
11298
11412
  this.onSearchEvent('')
11299
11413
  return
@@ -12262,6 +12376,7 @@ export default {
12262
12376
  * @return {void}
12263
12377
  */
12264
12378
  onColumnsEvent(columns) {
12379
+ this.viewChanged = true
12265
12380
  if (this.isSelfFetchMode && this.list.visibleColumns) {
12266
12381
  this.list.visibleColumns.value = columns
12267
12382
  }
@@ -12309,6 +12424,7 @@ export default {
12309
12424
  * @return {void}
12310
12425
  */
12311
12426
  onColumnsReorder(columns) {
12427
+ this.viewChanged = true
12312
12428
  this.onColumnsEvent(columns)
12313
12429
  },
12314
12430
 
@@ -12319,6 +12435,7 @@ export default {
12319
12435
  * @return {void}
12320
12436
  */
12321
12437
  onPinChange(count) {
12438
+ this.viewChanged = true
12322
12439
  if (!this.personalColumns || Array.isArray(this.appliedViewColumns)) {
12323
12440
  return
12324
12441
  }
@@ -13213,6 +13330,7 @@ export default {
13213
13330
  */
13214
13331
  onApplySavedView(view, { keepSavedViewKey = false } = {}) {
13215
13332
  this.appliedSavedViewId = view && (view.id || view.slug) ? String(view.id || view.slug) : ''
13333
+ this.lensChosenByHand = false
13216
13334
  const state = extractViewState(view)
13217
13335
  // A view's filters may carry tokens (`@me`, `@myTeam`): resolved here,
13218
13336
  // and an optional one that answers nothing (no team) is dropped. A
@@ -13221,6 +13339,10 @@ export default {
13221
13339
  loadMyTeam().then(() => this.onApplySavedView(view, { keepSavedViewKey }))
13222
13340
  return
13223
13341
  }
13342
+ // An opened view is unchanged until the reader changes it.
13343
+ this.$nextTick(() => {
13344
+ this.viewChanged = false
13345
+ })
13224
13346
  const ctx = typeof this.selfFetchTokenCtx === 'function' ? this.selfFetchTokenCtx() : {}
13225
13347
  state.filters = dropOptionalUnresolved(resolveFilterTokens(state.filters, ctx))
13226
13348
  this.appliedViewFiltersKey = this.viewFiltersKey(state.filters)
@@ -13346,6 +13468,7 @@ export default {
13346
13468
  }
13347
13469
  this.savedViews = [...this.savedViews, view]
13348
13470
  this.showSaveViewDialog = false
13471
+ this.viewChanged = false
13349
13472
  this.sidebarActiveTab = 'filters-tab'
13350
13473
  this.sidebarOpen = true
13351
13474
  this.toastSavedView('success', t('nextcloud-vue', 'View "{name}" saved', { name }))
@@ -32,11 +32,17 @@
32
32
  </div>
33
33
 
34
34
  <div class="cn-index-sidebar__section">
35
+ <!-- The board look names the field by its hint, as the list's
36
+ own search field, with the magnifier (DqZakenSorteren). -->
35
37
  <NcTextField
36
38
  :modelValue="searchValue || ''"
37
39
  :placeholder="searchPlaceholder"
38
- :label="searchLabel"
39
- @update:modelValue="$emit('search', $event)" />
40
+ :label="isBoardLook ? searchPlaceholder : searchLabel"
41
+ @update:modelValue="$emit('search', $event)">
42
+ <template v-if="isBoardLook" #icon>
43
+ <Magnify :size="18" />
44
+ </template>
45
+ </NcTextField>
40
46
  </div>
41
47
 
42
48
  <!-- Sort (list, card and map views; the table sorts by its headers):
@@ -355,6 +361,7 @@ import DragVertical from 'vue-material-design-icons/DragVertical.vue'
355
361
  import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
356
362
  import FilterRemoveOutline from 'vue-material-design-icons/FilterRemoveOutline.vue'
357
363
  import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
364
+ import Magnify from 'vue-material-design-icons/Magnify.vue'
358
365
  import Pin from 'vue-material-design-icons/Pin.vue'
359
366
  import PinOutline from 'vue-material-design-icons/PinOutline.vue'
360
367
  import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'
@@ -410,6 +417,7 @@ export default {
410
417
  Pin,
411
418
  PinOutline,
412
419
  InformationOutline,
420
+ Magnify,
413
421
  },
414
422
 
415
423
  inject: {
@@ -720,6 +720,7 @@ export default {
720
720
  * - `hidden` (boolean) — Hide this property
721
721
  * - `editable` (boolean) — Override editability (default: based on schema readOnly)
722
722
  * - `label` (string) — Override the display label
723
+ * - `emptyValue` (string) — Words shown, translated, for an empty or false value instead of a dash
723
724
  * - `widget` (string) — Override the widget type for editing
724
725
  * - `formatter` (string) — Display the value through the formatter
725
726
  * registered under this id on CnAppRoot (`cnFormatters`), called as
@@ -739,7 +740,8 @@ export default {
739
740
  },
740
741
 
741
742
  /**
742
- * Number of grid columns.
743
+ * Number of grid columns. The board look fits 200px columns instead,
744
+ * except for one column, which it keeps.
743
745
  */
744
746
  columns: {
745
747
  type: Number,
@@ -1100,7 +1102,8 @@ export default {
1100
1102
  // still showing the stored code — `submitted` under a Dutch
1101
1103
  // label, beside a list that already read "Ingediend".
1102
1104
  const enumLabel = this.enumDisplayLabel(field, raw)
1103
- values[field.key] = enumLabel !== null ? enumLabel : formatValue(raw, prop || {})
1105
+ const empty = this.emptyValueFor(field, raw)
1106
+ values[field.key] = empty !== null ? empty : (enumLabel !== null ? enumLabel : formatValue(raw, prop || {}))
1104
1107
  }
1105
1108
  return values
1106
1109
  },
@@ -1116,8 +1119,12 @@ export default {
1116
1119
  * CSS grid template for the container.
1117
1120
  */
1118
1121
  gridStyle() {
1119
- // The board look: as many 200px columns as the card has room for.
1122
+ // The board look: as many 200px columns as the card has room for,
1123
+ // unless the config asks for one column (a stacked list of pairs).
1120
1124
  if (normalizeLook(this.cnLook) === 'board') {
1125
+ if (this.columns === 1) {
1126
+ return { 'grid-template-columns': 'minmax(0, 1fr)' }
1127
+ }
1121
1128
  return {
1122
1129
  'grid-template-columns': 'repeat(auto-fit, minmax(200px, 1fr))',
1123
1130
  }
@@ -1842,6 +1849,24 @@ export default {
1842
1849
  return this.isPlainObject(raw)
1843
1850
  },
1844
1851
 
1852
+ /**
1853
+ * The words an override gives an empty or false value
1854
+ * (`overrides[key].emptyValue`, translated), or null to format it as usual.
1855
+ *
1856
+ * @param {object} field - Resolved field definition from resolvedFields
1857
+ * @param {*} raw - The field's value
1858
+ * @return {string|null}
1859
+ * @spec openspec/changes/data-widget-empty-value-one-column/specs/data-widget/spec.md
1860
+ */
1861
+ emptyValueFor(field, raw) {
1862
+ const override = this.resolvedOverrides[field.key]
1863
+ const words = override && typeof override.emptyValue === 'string' ? override.emptyValue : ''
1864
+ if (words === '' || !(raw === null || raw === undefined || raw === '' || raw === false)) {
1865
+ return null
1866
+ }
1867
+ return this.cnTranslate(words)
1868
+ },
1869
+
1845
1870
  /**
1846
1871
  * Check if a field is editable.
1847
1872
  *
@@ -860,7 +860,7 @@ export default {
860
860
  // `secondary` (a second, muted line under the cell) rides
861
861
  // along too: the table draws it, and dropping it here took the
862
862
  // list rows' second line away (zuiddrecht-pixel-gaps-3).
863
- for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow']) {
863
+ for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow', 'cellClass']) {
864
864
  if (c[k] !== undefined) {
865
865
  out[k] = c[k]
866
866
  }
@@ -117,6 +117,13 @@ import { readPath } from '../../utils/readPath.js'
117
117
  /** The bus channel every endpoint-bound widget on the page refetches on. */
118
118
  const PAGE_REFRESH_CHANNEL = 'cn:page:refresh'
119
119
 
120
+ /**
121
+ * The document event a recorded note sends, so a row's leaf (humaniq's
122
+ * hours) can record what it holds for the same object at the same moment.
123
+ * `detail` is the row's leaf props plus `note`.
124
+ */
125
+ const NOTE_RECORDED_EVENT = 'cn-record-notes:recorded'
126
+
120
127
  /**
121
128
  * The rows of a record list drawn as notes to record (`presentation:
122
129
  * "notes"`, DqDagAfsluiten "Nog open van vandaag"): per row its pill, title,
@@ -351,6 +358,9 @@ export default {
351
358
  try {
352
359
  const { default: axios } = await import('@nextcloud/axios')
353
360
  await axios.post(prefixUrl(url), { [this.action.bodyField || 'message']: text })
361
+ if (this.leaf) {
362
+ document.dispatchEvent(new CustomEvent(NOTE_RECORDED_EVENT, { detail: { ...this.leafProps(item.row), note: text } }))
363
+ }
354
364
  const { [item.key]: done, ...rest } = this.drafts
355
365
  this.drafts = rest
356
366
  if (refresh) {
@@ -3,7 +3,7 @@
3
3
  SPDX-License-Identifier: EUPL-1.2
4
4
  -->
5
5
  <template>
6
- <div class="cn-stacked-bar" :class="{ 'cn-stacked-bar--inset': inset }" data-testid="cn-stacked-bar">
6
+ <div class="cn-stacked-bar" :class="{ 'cn-stacked-bar--inset': inset, 'cn-stacked-bar--compact': compact }" data-testid="cn-stacked-bar">
7
7
  <div v-if="isLoading" class="cn-stacked-bar__state" role="status">
8
8
  <NcLoadingIcon :size="20" />
9
9
  <span>{{ loadingLabel }}</span>
@@ -183,7 +183,7 @@ export default {
183
183
  * segments to the card (`header` event, `count`), for a chip after the
184
184
  * title or a `{count}` in it.
185
185
  *
186
- * @type {{inset?: boolean, headerCount?: boolean, source?: {register?: string, schema?: string, groupBy?: string, filter?: object}, segments?: Array<{key?: string, label: string, value: number}>, order?: Array<string>, labels?: {[key: string]: string}, emptyText?: string}}
186
+ * @type {{inset?: boolean, compact?: boolean, headerCount?: boolean, source?: {register?: string, schema?: string, groupBy?: string, filter?: object}, segments?: Array<{key?: string, label: string, value: number}>, order?: Array<string>, labels?: {[key: string]: string}, emptyText?: string}}
187
187
  */
188
188
  content: {
189
189
  type: Object,
@@ -241,6 +241,17 @@ export default {
241
241
  return Boolean(this.content && this.content.inset === true)
242
242
  },
243
243
 
244
+ /**
245
+ * Whether the widget is the compact summary box (`content.compact`):
246
+ * a 15px heading, one thin bar and the legend on one line.
247
+ *
248
+ * @spec openspec/changes/stacked-bar-compact/specs/stacked-bar-compact/spec.md#requirement-a-stacked-bar-can-be-drawn-as-the-compact-summary-box
249
+ * @return {boolean}
250
+ */
251
+ compact() {
252
+ return Boolean(this.content && this.content.compact === true)
253
+ },
254
+
244
255
  /**
245
256
  * The translate function in use.
246
257
  *
@@ -651,4 +662,59 @@ export default {
651
662
  line-height: 1.2;
652
663
  color: var(--color-main-text);
653
664
  }
665
+
666
+ /* `content.compact: true`: the summary box (DqZaakDocumenten "4 van 7
667
+ beoordeeld"): the sunken ground, a 15px heading, one 8px bar of joined
668
+ segments and the legend on one line, each count beside its label. */
669
+ .cn-stacked-bar--compact {
670
+ box-sizing: border-box;
671
+ gap: 10px;
672
+ padding: 14px 16px;
673
+ border-radius: 10px;
674
+ background: var(--cn-board-sunken-ground, var(--color-background-hover));
675
+ }
676
+
677
+ .cn-stacked-bar--compact .cn-stacked-bar__heading {
678
+ flex-wrap: wrap;
679
+ gap: 8px;
680
+ font-size: 15px;
681
+ }
682
+
683
+ .cn-stacked-bar--compact .cn-stacked-bar__bar {
684
+ gap: 0;
685
+ height: 8px;
686
+ border-radius: 4px;
687
+ overflow: hidden;
688
+ }
689
+
690
+ .cn-stacked-bar--compact .cn-stacked-bar__segment {
691
+ border-radius: 0;
692
+ }
693
+
694
+ .cn-stacked-bar--compact .cn-stacked-bar__legend {
695
+ display: flex;
696
+ flex-wrap: wrap;
697
+ gap: 8px 18px;
698
+ font-size: 14px;
699
+ }
700
+
701
+ .cn-stacked-bar--compact .cn-stacked-bar__legend-item {
702
+ flex-direction: row;
703
+ align-items: center;
704
+ gap: 8px;
705
+ }
706
+
707
+ .cn-stacked-bar--compact .cn-stacked-bar__label {
708
+ color: var(--color-main-text);
709
+ font-size: 1em;
710
+ }
711
+
712
+ .cn-stacked-bar--compact .cn-stacked-bar__dot {
713
+ border-radius: 50%;
714
+ }
715
+
716
+ .cn-stacked-bar--compact .cn-stacked-bar__count {
717
+ font-size: 1em;
718
+ line-height: inherit;
719
+ }
654
720
  </style>