@conduction/nextcloud-vue 2.143.0 → 2.145.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 (165) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +7 -2
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +1 -0
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  7. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +55 -2
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  11. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +16 -0
  12. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  13. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -1
  14. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  15. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  16. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  17. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +1 -1
  18. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +3 -3
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
  24. package/dist/esm/components/CnEventsCard/CnEventsCard.vue.js +1 -1
  25. package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js +9 -0
  26. package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js.map +1 -1
  27. package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js +24 -13
  28. package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  30. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +2 -2
  31. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  34. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +2 -1
  35. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  36. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +9 -4
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  40. package/dist/esm/components/CnMailTab/CnMailTab.vue.js +1 -1
  41. package/dist/esm/components/CnMailTab/CnMailTab.vue2.js.map +1 -1
  42. package/dist/esm/components/CnMailTab/CnMailTab.vue3.js +2 -2
  43. package/dist/esm/components/CnMailTab/CnMailTab.vue3.js.map +1 -1
  44. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  45. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +3 -0
  46. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  47. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js +19 -14
  48. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  49. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
  50. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
  51. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
  52. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
  53. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +30 -0
  54. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
  55. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +158 -102
  56. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  58. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +73 -12
  59. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +165 -126
  61. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
  63. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
  64. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +12 -0
  65. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
  66. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +1 -1
  67. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +14 -0
  68. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -1
  69. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +2 -1
  70. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -1
  71. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue.js +1 -1
  72. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +110 -2
  73. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  74. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +146 -80
  75. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  76. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  77. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -8
  78. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  79. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  80. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue.js +1 -1
  81. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +10 -2
  82. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  83. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  84. package/dist/esm/composables/indexSources.js +35 -9
  85. package/dist/esm/composables/indexSources.js.map +1 -1
  86. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue.js +1 -1
  87. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js +45 -0
  88. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js.map +1 -1
  89. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js +67 -34
  90. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js.map +1 -1
  91. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue.js +1 -1
  92. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js +3 -22
  93. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js.map +1 -1
  94. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue3.js.map +1 -1
  95. package/dist/esm/integrations/builtin/calendar/eventStatus.js +67 -0
  96. package/dist/esm/integrations/builtin/calendar/eventStatus.js.map +1 -0
  97. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
  98. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +39 -0
  99. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
  100. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +81 -32
  101. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
  102. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue.js +1 -1
  103. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +5 -6
  104. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
  105. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
  106. package/dist/esm/integrations/builtin/deck/deckCardParts.js +59 -0
  107. package/dist/esm/integrations/builtin/deck/deckCardParts.js.map +1 -0
  108. package/dist/esm/l10n/en.json.js +6 -0
  109. package/dist/esm/l10n/en.json.js.map +1 -1
  110. package/dist/esm/l10n/nl.json.js +8 -0
  111. package/dist/esm/l10n/nl.json.js.map +1 -1
  112. package/dist/esm/library-style.js +1 -1
  113. package/dist/esm/nextcloud-vue.css +1505 -637
  114. package/dist/esm/utils/builtInFormatters.js +18 -1
  115. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  116. package/dist/esm/utils/fetchFilterCounts.js +5 -2
  117. package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
  118. package/dist/esm/utils/localHeaderRows.js +95 -0
  119. package/dist/esm/utils/localHeaderRows.js.map +1 -0
  120. package/dist/esm/utils/nextcloudDirectory.js +19 -6
  121. package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
  122. package/dist/nextcloud-vue.cjs.js +1848 -924
  123. package/dist/nextcloud-vue.cjs.js.map +2 -2
  124. package/dist/nextcloud-vue.css +1505 -637
  125. package/l10n/en.json +6 -0
  126. package/l10n/nl.json +8 -0
  127. package/package.json +1 -1
  128. package/src/components/CnActionDialog/CnActionDialog.vue +36 -4
  129. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +55 -2
  130. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +18 -0
  131. package/src/components/CnConversationThread/CnConversationThread.vue +5 -2
  132. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +2 -1
  133. package/src/components/CnEventsCard/CnEventsCard.vue +27 -5
  134. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +2 -2
  135. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +15 -1
  136. package/src/components/CnIndexPage/CnIndexPage.vue +9 -4
  137. package/src/components/CnMailTab/CnMailTab.vue +6 -4
  138. package/src/components/CnNotesCard/CnNotesCard.vue +8 -0
  139. package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +4 -1
  140. package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +114 -46
  141. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +131 -20
  142. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +26 -1
  143. package/src/components/CnSettingsField/CnSettingsField.vue +15 -0
  144. package/src/components/CnTasksWidget/CnTasksWidget.vue +170 -13
  145. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -8
  146. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +10 -2
  147. package/src/composables/indexSources.js +35 -9
  148. package/src/css/dialog.css +25 -10
  149. package/src/css/kpi-card.css +3 -0
  150. package/src/css/look-board-detail.css +513 -0
  151. package/src/css/look-board-index.css +52 -4
  152. package/src/css/look-board.css +163 -8
  153. package/src/css/table.css +8 -1
  154. package/src/integrations/builtin/calendar/CnCalendarCard.vue +76 -11
  155. package/src/integrations/builtin/calendar/CnCalendarTab.vue +3 -22
  156. package/src/integrations/builtin/calendar/__tests__/CnCalendarCardBoardRows.spec.js +52 -0
  157. package/src/integrations/builtin/calendar/eventStatus.js +63 -0
  158. package/src/integrations/builtin/deck/CnDeckCard.vue +66 -0
  159. package/src/integrations/builtin/deck/CnDeckTab.vue +5 -6
  160. package/src/integrations/builtin/deck/__tests__/CnDeckCardBoardRows.spec.js +49 -0
  161. package/src/integrations/builtin/deck/deckCardParts.js +56 -0
  162. package/src/utils/builtInFormatters.js +17 -0
  163. package/src/utils/fetchFilterCounts.js +5 -2
  164. package/src/utils/localHeaderRows.js +91 -0
  165. package/src/utils/nextcloudDirectory.js +19 -6
@@ -2,6 +2,7 @@ import { translate, translatePlural } from '@nextcloud/l10n';
2
2
  import { formatRelativeDate } from './boardDate.js';
3
3
  import { formatMoney } from './formatMetric.js';
4
4
  import { parseDuration } from './isoDuration.js';
5
+ import { userDisplayName } from './userDisplayName.js';
5
6
 
6
7
  /**
7
8
  * @file Built-in formatters merged into the `cnFormatters` registry
@@ -523,6 +524,21 @@ function formatIsoDuration(value) {
523
524
  return words[period.unit] ? words[period.unit]() : String(value)
524
525
  }
525
526
 
527
+ /**
528
+ * A Nextcloud user id as its display name (`userName` registry key): the id
529
+ * while the name is looked up, and for a user nobody answers for.
530
+ *
531
+ * @spec openspec/changes/user-name-formatter/specs/user-name-formatter/spec.md#requirement-a-user-id-can-read-as-its-display-name
532
+ * @param {unknown} value The user id.
533
+ * @return {string} The display name, the id, or ''.
534
+ */
535
+ function formatUserName(value) {
536
+ if (value === null || value === undefined || value === '' || typeof value === 'object') {
537
+ return ''
538
+ }
539
+ return userDisplayName(value) || String(value)
540
+ }
541
+
526
542
  const BUILT_IN_FORMATTERS = {
527
543
  date: formatDate,
528
544
  datetime: formatDateTime,
@@ -538,7 +554,8 @@ const BUILT_IN_FORMATTERS = {
538
554
  masked: formatMasked,
539
555
  isoDuration: formatIsoDuration,
540
556
  valueMap: formatValueMap,
557
+ userName: formatUserName,
541
558
  };
542
559
 
543
- export { BUILT_IN_FORMATTERS, formatConditionalPhrase, formatConnectionSettingsLabel, formatConnectionStatus, formatCount, formatCurrency, formatDate, formatDateTime, formatDaysSince, formatDaysUntil, formatIsoDuration, formatMasked, formatRelativeDay, formatRelativeTime, formatValueMap };
560
+ export { BUILT_IN_FORMATTERS, formatConditionalPhrase, formatConnectionSettingsLabel, formatConnectionStatus, formatCount, formatCurrency, formatDate, formatDateTime, formatDaysSince, formatDaysUntil, formatIsoDuration, formatMasked, formatRelativeDay, formatRelativeTime, formatUserName, formatValueMap };
544
561
  //# sourceMappingURL=builtInFormatters.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"builtInFormatters.js","sources":["../../../src/utils/builtInFormatters.js"],"sourcesContent":["import { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { formatRelativeDate } from './boardDate.js'\nimport { formatMoney } from './formatMetric.js'\nimport { parseDuration } from './isoDuration.js'\n\n/**\n * @file Built-in formatters merged into the `cnFormatters` registry\n * `CnAppRoot` provides to descendants.\n *\n * Resolution order (in `CnCellRenderer`):\n * 1. Consumer-registered `formatters[id]` (passed to CnAppRoot).\n * 2. Built-in `BUILT_IN_FORMATTERS[id]` from this file.\n * 3. Schema-type-aware rendering.\n * 4. Plain `formatValue()` fallback.\n *\n * Consumer formatters win (CnAppRoot spreads\n * `{ ...BUILT_IN_FORMATTERS, ...props.formatters }`).\n *\n * A formatter is invoked as `fn(value, row, property, options)` — `options`\n * is the column's declarative `formatterOptions` map (undefined when the\n * column declares none), so config-driven formatters (`currency`,\n * `conditionalPhrase`) work without one registry function per configuration.\n * The fourth argument is additive: pre-existing three-argument formatters\n * are unaffected.\n *\n * Every built-in MUST be safe against null / empty / non-parseable\n * inputs — the contract is \"return the original value (or empty\n * string) rather than throw\".\n */\n\n/**\n * Parse a value that's meant to be a date — accepts a `Date` instance,\n * an ISO/parseable string, or a numeric timestamp.\n *\n * @param {Date|string|number|null|undefined} value The candidate date value.\n * @return {Date|null} A `Date` instance, or `null` if the input is null/empty/unparseable.\n */\nfunction toDate(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn null\n\t}\n\tif (value instanceof Date) {\n\t\treturn Number.isNaN(value.getTime()) ? null : value\n\t}\n\tconst d = new Date(value)\n\treturn Number.isNaN(d.getTime()) ? null : d\n}\n\n/**\n * Locale-formatted date (no time). Backed by `Intl.DateTimeFormat`\n * (`dateStyle: 'medium'`) using the user-agent locale.\n *\n * @param {unknown} value A `Date`, parseable date string, or timestamp.\n * @return {string} Formatted date, or `''` for null/empty, or `String(value)` for unparseable.\n */\nexport function formatDate(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst d = toDate(value)\n\tif (!d) {\n\t\treturn String(value)\n\t}\n\treturn new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(d)\n}\n\n/**\n * Locale-formatted date + time. Backed by `Intl.DateTimeFormat`\n * (`dateStyle: 'medium'`, `timeStyle: 'short'`).\n *\n * @param {unknown} value A `Date`, parseable date string, or timestamp.\n * @return {string} Formatted date + time, or `''` for null/empty, or `String(value)` for unparseable.\n */\nexport function formatDateTime(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst d = toDate(value)\n\tif (!d) {\n\t\treturn String(value)\n\t}\n\treturn new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(d)\n}\n\n/**\n * Relative time (\"3 days ago\") via `Intl.RelativeTimeFormat`.\n * Picks the coarsest unit whose absolute delta exceeds one unit,\n * down to minutes (anything sub-minute clamps to \"now\"/seconds).\n *\n * @param {unknown} value A `Date`, parseable date string, or timestamp.\n * @return {string} Relative phrasing, or `''` for null/empty, or `String(value)` for unparseable.\n */\nexport function formatRelativeTime(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst d = toDate(value)\n\tif (!d) {\n\t\treturn String(value)\n\t}\n\tconst diffMs = d.getTime() - Date.now()\n\tconst rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })\n\tconst units = [\n\t\t['year', 31536000000],\n\t\t['month', 2592000000],\n\t\t['week', 604800000],\n\t\t['day', 86400000],\n\t\t['hour', 3600000],\n\t\t['minute', 60000],\n\t]\n\tfor (const [unit, ms] of units) {\n\t\tif (Math.abs(diffMs) >= ms || unit === 'minute') {\n\t\t\treturn rtf.format(Math.round(diffMs / ms), unit)\n\t\t}\n\t}\n\treturn rtf.format(0, 'second')\n}\n\n/**\n * Whole-day difference between a date value and today, comparing\n * date-only (both clamped to local midnight) so a due date later today\n * still counts as \"today\", not \"in 0.4 days\".\n *\n * @param {Date} d The parsed date.\n * @return {number} Signed day count: positive = future, negative = past, 0 = today.\n */\nfunction dayDiffFromToday(d) {\n\tconst target = new Date(d)\n\ttarget.setHours(0, 0, 0, 0)\n\tconst today = new Date()\n\ttoday.setHours(0, 0, 0, 0)\n\treturn Math.round((target.getTime() - today.getTime()) / 86400000)\n}\n\n/**\n * Future-oriented deadline formatter (`daysUntil` registry key), i18n'd\n * through the library's own translation slug (`nextcloud-vue`):\n * a future date renders \"N days remaining\", today renders \"Due today\",\n * and a past date renders \"N days overdue\". Day counts are plural-aware\n * via `translatePlural`.\n *\n * Null-safe per the built-in-formatter contract: `''` for null/empty,\n * `String(value)` for unparseable input — never throws.\n *\n * @param {unknown} value A `Date`, parseable date string, or timestamp.\n * @return {string} The relative-day phrasing (or ''/original on bad input).\n */\nexport function formatDaysUntil(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst d = toDate(value)\n\tif (!d) {\n\t\treturn String(value)\n\t}\n\tconst days = dayDiffFromToday(d)\n\tif (days === 0) {\n\t\treturn t('nextcloud-vue', 'Due today')\n\t}\n\tif (days > 0) {\n\t\treturn n('nextcloud-vue', '{count} day remaining', '{count} days remaining', days, { count: days })\n\t}\n\tconst overdue = Math.abs(days)\n\treturn n('nextcloud-vue', '{count} day overdue', '{count} days overdue', overdue, { count: overdue })\n}\n\n/**\n * Elapsed-time formatter (`daysSince` registry key), i18n'd through the\n * library's own translation slug (`nextcloud-vue`) in the Nextcloud user\n * language. Whole calendar days, read as a running phrase so it sits after a\n * title or in a meta line: today renders \"today\", one day back renders\n * \"yesterday\", further back renders \"N days ago\". A (nonsensical but\n * harmless) future date renders \"tomorrow\" or \"in N days\". Day counts are\n * plural-aware via `translatePlural`.\n *\n * Null-safe per the built-in-formatter contract: `''` for null/empty,\n * `String(value)` for unparseable input, never throws.\n *\n * @param {unknown} value A `Date`, parseable date string, or timestamp.\n * @return {string} The relative-day phrasing (or ''/original on bad input).\n */\nexport function formatDaysSince(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst d = toDate(value)\n\tif (!d) {\n\t\treturn String(value)\n\t}\n\tconst days = dayDiffFromToday(d)\n\tif (days === 0) {\n\t\treturn t('nextcloud-vue', 'today')\n\t}\n\tif (days === -1) {\n\t\treturn t('nextcloud-vue', 'yesterday')\n\t}\n\tif (days === 1) {\n\t\treturn t('nextcloud-vue', 'tomorrow')\n\t}\n\tif (days < 0) {\n\t\tconst ago = Math.abs(days)\n\t\treturn n('nextcloud-vue', '{count} day ago', '{count} days ago', ago, { count: ago })\n\t}\n\treturn n('nextcloud-vue', 'in {count} day', 'in {count} days', days, { count: days })\n}\n\n/**\n * Relative-date formatter (`relativeDate` registry key): \"today\", \"yesterday\",\n * then the board's short date (\"5 okt\"). The column's `formatterOptions` may set\n * `time: true` to add the time of day after the day (\"vandaag, 09.42\").\n *\n * Null-safe per the built-in-formatter contract: `''` for null/empty,\n * `String(value)` for unparseable input, never throws.\n *\n * @param {unknown} value A `Date`, parseable date string, or timestamp.\n * @param {object} [_row] The full row (unused).\n * @param {object} [_property] The schema property (unused).\n * @param {{time?: boolean}} [options] The column's `formatterOptions`.\n * @return {string} The relative-day phrasing (or ''/original on bad input).\n * @spec openspec/changes/screens-relative-dates/specs/relative-dates/spec.md#requirement-a-relative-date-reads-today-yesterday-then-a-short-date\n */\nexport function formatRelativeDay(value, _row, _property, options) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst text = formatRelativeDate(value, { withTime: options?.time === true })\n\treturn text === '' ? String(value) : text\n}\n\n/**\n * Currency formatter (`currency` registry key) via `Intl.NumberFormat`.\n * Defaults to EUR; the column's `formatterOptions` may set `currency`\n * (ISO-4217 code, guarded by `safeCurrencyCode` so an invalid code can\n * never throw a `RangeError`) and `decimals` (default 2).\n *\n * Null-safe per the built-in-formatter contract: `''` for null/empty,\n * `String(value)` for non-numeric input — never throws.\n *\n * @param {unknown} value A numeric value (or numeric string).\n * @param {object} [row] The full row, for `currencyField`.\n * @param {object} [_property] The schema property (unused).\n * @param {{currency?: string, currencyField?: string, decimals?: number}} [options] The column's `formatterOptions`.\n * @return {string} The locale currency string (or ''/original on bad input).\n */\nexport function formatCurrency(value, row, _property, options) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn String(value)\n\t}\n\treturn formatMoney(num, options || {}, row)\n}\n\n/**\n * Sign/zero-based phrase selector (`conditionalPhrase` registry key) —\n * generalizes `daysUntil` to ANY numeric field: the column's\n * `formatterOptions` supply pre-translated `{ negative, zero, positive }`\n * phrases and the matching one renders with `{n}` replaced by the ABSOLUTE\n * value (so \"{n} days overdue\" reads naturally for -3). Phrases are\n * consumer-provided strings — apps translate them before declaring the\n * column, keeping i18n in the app layer.\n *\n * Null-safe per the built-in-formatter contract: `''` for null/empty,\n * `String(value)` for non-numeric input or when the selected phrase is\n * missing — never throws.\n *\n * @param {unknown} value A numeric value (or numeric string).\n * @param {object} [_row] The full row (unused).\n * @param {object} [_property] The schema property (unused).\n * @param {{negative?: string, zero?: string, positive?: string}} [options] The column's `formatterOptions`.\n * @return {string} The selected phrase with `{n}` substituted (or ''/original on bad input).\n */\nexport function formatConditionalPhrase(value, _row, _property, options) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn String(value)\n\t}\n\tconst opts = options || {}\n\tconst phrase = num < 0 ? opts.negative : (num > 0 ? opts.positive : opts.zero)\n\tif (typeof phrase !== 'string' || phrase === '') {\n\t\treturn String(value)\n\t}\n\treturn phrase.replace(/\\{n\\}/g, String(Math.abs(num)))\n}\n\n/**\n * Read a value that may be a collection SERIALISED as JSON back into the\n * collection itself. OpenRegister hands a stringified array back for some\n * property types (and an app's own import path may persist one), which\n * `formatCount` would otherwise report as a single entry.\n *\n * Only `[`/`{`-leading strings are tried, so an ordinary text value never pays\n * for a parse attempt, and a string that merely looks like JSON but isn't falls\n * back to being one entry.\n *\n * @param {unknown} value The raw cell value.\n * @return {unknown} The parsed collection, or `value` unchanged.\n */\nfunction parseCollection(value) {\n\tif (typeof value !== 'string') {\n\t\treturn value\n\t}\n\tconst trimmed = value.trim()\n\tif (trimmed[0] !== '[' && trimmed[0] !== '{') {\n\t\treturn value\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(trimmed)\n\t\treturn (parsed !== null && typeof parsed === 'object') ? parsed : value\n\t} catch {\n\t\treturn value\n\t}\n}\n\n/**\n * Entry-count summariser (`count` registry key) — renders a collection-valued\n * cell as \"5 frames\" instead of the truncated JSON blob `formatValue` would\n * produce. Counts array entries or object keys; a PRESENT scalar counts as 1.\n * The column's `formatterOptions` supply pre-translated\n * `{ singular, plural, zero }` phrases with `{n}` substituted, following the\n * `conditionalPhrase` convention of keeping i18n in the app layer. Without\n * phrases it renders the bare count.\n *\n * Null-safe per the built-in-formatter contract: `zero` (or `''`) for\n * null/empty — never throws.\n *\n * @param {unknown} value An array, object, JSON-encoded collection, or scalar.\n * @param {object} [_row] The full row (unused).\n * @param {object} [_property] The schema property (unused).\n * @param {{singular?: string, plural?: string, zero?: string}} [options] The column's `formatterOptions`.\n * @return {string} The selected phrase with `{n}` substituted, or the bare count.\n */\nexport function formatCount(value, _row, _property, options) {\n\tconst opts = options || {}\n\tconst zero = typeof opts.zero === 'string' ? opts.zero : ''\n\t// Every EMPTY value takes the zero phrase, not just null/''. `0` and `false`\n\t// used to fall through to the scalar branch and count as one entry, so a\n\t// column over a `0` rendered \"1 retry\" — the singular of a thing that isn't\n\t// there.\n\tif (value === null || value === undefined || value === '' || value === 0 || value === false) {\n\t\treturn zero\n\t}\n\tconst collection = parseCollection(value)\n\tlet n\n\tif (Array.isArray(collection)) {\n\t\tn = collection.length\n\t} else if (typeof collection === 'object') {\n\t\tn = Object.keys(collection).length\n\t} else {\n\t\tn = 1\n\t}\n\tif (n === 0) {\n\t\treturn zero\n\t}\n\tconst phrase = n === 1 ? (opts.singular ?? opts.plural) : (opts.plural ?? opts.singular)\n\tif (typeof phrase !== 'string' || phrase === '') {\n\t\treturn String(n)\n\t}\n\treturn phrase.replace(/\\{n\\}/g, String(n))\n}\n\n/**\n * The seven stored connection status values and their English source labels.\n *\n * The label is translated on each call, not here. A module-level `t()` runs\n * before the catalogue is registered and would freeze every label in English.\n */\nconst CONNECTION_STATUS_LABELS = {\n\tconfigured: 'Configured',\n\tlimited: 'Limited',\n\tunconfigured: 'Not configured',\n\tsimulated: 'Simulated',\n\tdisabled: 'Switched off',\n\tunavailable: 'Not available',\n\terror: 'Error',\n}\n\n/**\n * The short labels `formatterOptions.short` draws (board DqInstellingen\n * \"Aan / Uit / Fout\"): a connection that works in some form is on, one that\n * does not run is off, and a failing one is an error.\n */\nconst CONNECTION_STATUS_SHORT_LABELS = {\n\tconfigured: 'On',\n\tlimited: 'On',\n\tsimulated: 'On',\n\tunconfigured: 'Off',\n\tdisabled: 'Off',\n\tunavailable: 'Off',\n\terror: 'Error',\n}\n\n/**\n * Connection status formatter (`connectionStatus` registry key) for the\n * fleet connection registry. Renders one of the seven stored status values\n * as its translated label through the library's own slug (`nextcloud-vue`).\n * With `formatterOptions.short` it renders the board's three short labels\n * instead: On (configured, limited, simulated), Off (not configured,\n * switched off, not available) and Error.\n *\n * An unknown value passes through unchanged, so a status added later still\n * shows as its raw value instead of an empty cell. Null and undefined render\n * as `''`. Never throws.\n *\n * @param {unknown} value The stored status value.\n * @param {object} [row] The row (unused).\n * @param {object} [property] The column's property (unused).\n * @param {{short?: boolean}} [options] The column's `formatterOptions`.\n * @return {string} The translated label, or the value itself when it is not one of the seven.\n */\nexport function formatConnectionStatus(value, row, property, options) {\n\tif (value === null || value === undefined) {\n\t\treturn ''\n\t}\n\tconst labels = options && options.short === true ? CONNECTION_STATUS_SHORT_LABELS : CONNECTION_STATUS_LABELS\n\tconst label = Object.hasOwn(labels, value) ? labels[value] : null\n\treturn label ? t('nextcloud-vue', label) : String(value)\n}\n\n/**\n * Connection settings label formatter (`connectionSettingsLabel` registry\n * key). Renders \"Open settings\" when the row's `settingsUrl` is a non-empty\n * string, and `''` otherwise. Paired with the built-in `link` cell widget, an\n * empty label and href fall through to plain empty text, so a connection with\n * no settings section offers nothing to click.\n *\n * @param {unknown} value The row's `settingsUrl`.\n * @return {string} The translated link text, or `''`.\n */\nexport function formatConnectionSettingsLabel(value) {\n\treturn typeof value === 'string' && value !== ''\n\t\t? t('nextcloud-vue', 'Open settings')\n\t\t: ''\n}\n\n/**\n * Show a value with all but its last characters replaced by `*`, as the facts\n * widget's `masked` format does: an IBAN or a BSN reads `*****4567`.\n *\n * DISPLAY ONLY, NOT ACCESS CONTROL. The full value is still in the row the\n * page received; a reader with the browser's developer tools can read it.\n * A value the reader must not have belongs hidden in OpenRegister (property\n * authorization), not masked here.\n *\n * The column's `formatterOptions.visible` sets how many characters stay\n * (default 4); at most five `*` are drawn, so the length does not leak.\n * Null-safe: `''` for null or empty.\n *\n * @param {unknown} value The value.\n * @param {object} [_row] The full row (unused).\n * @param {object} [_property] The schema property (unused).\n * @param {{visible?: number}} [options] The column's `formatterOptions`.\n * @return {string} The masked value.\n * @spec openspec/changes/masked-formatter-max-aggregate/specs/index-page/spec.md#requirement-a-column-can-mask-its-value-for-display\n */\nexport function formatMasked(value, _row, _property, options) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst text = String(value)\n\tconst visible = Number.isInteger(options && options.visible) && options.visible >= 0 ? options.visible : 4\n\tif (text.length <= visible) {\n\t\treturn text\n\t}\n\treturn '*'.repeat(Math.min(text.length - visible, 5)) + (visible > 0 ? text.slice(-visible) : '')\n}\n\n/**\n * A stored value through a display map (`valueMap` registry key): the label\n * `options.map` gives the value, else the value as stored. \"Besloten\n * Vennootschap\" reads \"BV\" on DqOrganisaties.\n *\n * @param {unknown} value The stored value.\n * @param {object} [_row] The full row (unused).\n * @param {object} [_property] The schema property (unused).\n * @param {{map?: Object<string, string>}} [options] The column's `formatterOptions`.\n * @return {string} The label, or the value.\n * @spec openspec/changes/value-map-formatter/specs/index-page/spec.md#requirement-a-column-can-show-stored-values-through-a-display-map\n */\nexport function formatValueMap(value, _row, _property, options) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst map = options && options.map && typeof options.map === 'object' ? options.map : {}\n\tconst label = Object.hasOwn(map, String(value)) ? map[String(value)] : undefined\n\treturn typeof label === 'string' && label !== '' ? label : String(value)\n}\n\n/**\n * Built-in formatter registry merged under any consumer-registered\n * `formatters` in `CnAppRoot`'s `cnFormatters` provide.\n */\n/**\n * An ISO-8601 period of one unit in words (`isoDuration` registry key):\n * `P10Y` reads \"10 years\", `P6W` \"6 weeks\". A retention period or a term is\n * stored as such a period and was shown raw. Anything that is not a one-unit\n * period is returned as written, `''` for null or empty.\n *\n * @param {unknown} value The stored period.\n * @return {string} The period in words.\n */\nexport function formatIsoDuration(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst period = parseDuration(String(value))\n\tif (period === null) {\n\t\treturn String(value)\n\t}\n\tconst words = {\n\t\tyears: () => n('nextcloud-vue', '%n year', '%n years', period.amount),\n\t\tmonths: () => n('nextcloud-vue', '%n month', '%n months', period.amount),\n\t\tweeks: () => n('nextcloud-vue', '%n week', '%n weeks', period.amount),\n\t\tdays: () => n('nextcloud-vue', '%n day', '%n days', period.amount),\n\t\thours: () => n('nextcloud-vue', '%n hour', '%n hours', period.amount),\n\t\tminutes: () => n('nextcloud-vue', '%n minute', '%n minutes', period.amount),\n\t}\n\treturn words[period.unit] ? words[period.unit]() : String(value)\n}\n\nexport const BUILT_IN_FORMATTERS = {\n\tdate: formatDate,\n\tdatetime: formatDateTime,\n\t'relative-time': formatRelativeTime,\n\tdaysSince: formatDaysSince,\n\tdaysUntil: formatDaysUntil,\n\trelativeDate: formatRelativeDay,\n\tcurrency: formatCurrency,\n\tconditionalPhrase: formatConditionalPhrase,\n\tcount: formatCount,\n\tconnectionStatus: formatConnectionStatus,\n\tconnectionSettingsLabel: formatConnectionSettingsLabel,\n\tmasked: formatMasked,\n\tisoDuration: formatIsoDuration,\n\tvalueMap: formatValueMap,\n}\n"],"names":["t","n"],"mappings":"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"}
1
+ {"version":3,"file":"builtInFormatters.js","sources":["../../../src/utils/builtInFormatters.js"],"sourcesContent":["import { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { formatRelativeDate } from './boardDate.js'\nimport { formatMoney } from './formatMetric.js'\nimport { parseDuration } from './isoDuration.js'\nimport { userDisplayName } from './userDisplayName.js'\n\n/**\n * @file Built-in formatters merged into the `cnFormatters` registry\n * `CnAppRoot` provides to descendants.\n *\n * Resolution order (in `CnCellRenderer`):\n * 1. Consumer-registered `formatters[id]` (passed to CnAppRoot).\n * 2. Built-in `BUILT_IN_FORMATTERS[id]` from this file.\n * 3. Schema-type-aware rendering.\n * 4. Plain `formatValue()` fallback.\n *\n * Consumer formatters win (CnAppRoot spreads\n * `{ ...BUILT_IN_FORMATTERS, ...props.formatters }`).\n *\n * A formatter is invoked as `fn(value, row, property, options)` — `options`\n * is the column's declarative `formatterOptions` map (undefined when the\n * column declares none), so config-driven formatters (`currency`,\n * `conditionalPhrase`) work without one registry function per configuration.\n * The fourth argument is additive: pre-existing three-argument formatters\n * are unaffected.\n *\n * Every built-in MUST be safe against null / empty / non-parseable\n * inputs — the contract is \"return the original value (or empty\n * string) rather than throw\".\n */\n\n/**\n * Parse a value that's meant to be a date — accepts a `Date` instance,\n * an ISO/parseable string, or a numeric timestamp.\n *\n * @param {Date|string|number|null|undefined} value The candidate date value.\n * @return {Date|null} A `Date` instance, or `null` if the input is null/empty/unparseable.\n */\nfunction toDate(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn null\n\t}\n\tif (value instanceof Date) {\n\t\treturn Number.isNaN(value.getTime()) ? null : value\n\t}\n\tconst d = new Date(value)\n\treturn Number.isNaN(d.getTime()) ? null : d\n}\n\n/**\n * Locale-formatted date (no time). Backed by `Intl.DateTimeFormat`\n * (`dateStyle: 'medium'`) using the user-agent locale.\n *\n * @param {unknown} value A `Date`, parseable date string, or timestamp.\n * @return {string} Formatted date, or `''` for null/empty, or `String(value)` for unparseable.\n */\nexport function formatDate(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst d = toDate(value)\n\tif (!d) {\n\t\treturn String(value)\n\t}\n\treturn new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(d)\n}\n\n/**\n * Locale-formatted date + time. Backed by `Intl.DateTimeFormat`\n * (`dateStyle: 'medium'`, `timeStyle: 'short'`).\n *\n * @param {unknown} value A `Date`, parseable date string, or timestamp.\n * @return {string} Formatted date + time, or `''` for null/empty, or `String(value)` for unparseable.\n */\nexport function formatDateTime(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst d = toDate(value)\n\tif (!d) {\n\t\treturn String(value)\n\t}\n\treturn new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(d)\n}\n\n/**\n * Relative time (\"3 days ago\") via `Intl.RelativeTimeFormat`.\n * Picks the coarsest unit whose absolute delta exceeds one unit,\n * down to minutes (anything sub-minute clamps to \"now\"/seconds).\n *\n * @param {unknown} value A `Date`, parseable date string, or timestamp.\n * @return {string} Relative phrasing, or `''` for null/empty, or `String(value)` for unparseable.\n */\nexport function formatRelativeTime(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst d = toDate(value)\n\tif (!d) {\n\t\treturn String(value)\n\t}\n\tconst diffMs = d.getTime() - Date.now()\n\tconst rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })\n\tconst units = [\n\t\t['year', 31536000000],\n\t\t['month', 2592000000],\n\t\t['week', 604800000],\n\t\t['day', 86400000],\n\t\t['hour', 3600000],\n\t\t['minute', 60000],\n\t]\n\tfor (const [unit, ms] of units) {\n\t\tif (Math.abs(diffMs) >= ms || unit === 'minute') {\n\t\t\treturn rtf.format(Math.round(diffMs / ms), unit)\n\t\t}\n\t}\n\treturn rtf.format(0, 'second')\n}\n\n/**\n * Whole-day difference between a date value and today, comparing\n * date-only (both clamped to local midnight) so a due date later today\n * still counts as \"today\", not \"in 0.4 days\".\n *\n * @param {Date} d The parsed date.\n * @return {number} Signed day count: positive = future, negative = past, 0 = today.\n */\nfunction dayDiffFromToday(d) {\n\tconst target = new Date(d)\n\ttarget.setHours(0, 0, 0, 0)\n\tconst today = new Date()\n\ttoday.setHours(0, 0, 0, 0)\n\treturn Math.round((target.getTime() - today.getTime()) / 86400000)\n}\n\n/**\n * Future-oriented deadline formatter (`daysUntil` registry key), i18n'd\n * through the library's own translation slug (`nextcloud-vue`):\n * a future date renders \"N days remaining\", today renders \"Due today\",\n * and a past date renders \"N days overdue\". Day counts are plural-aware\n * via `translatePlural`.\n *\n * Null-safe per the built-in-formatter contract: `''` for null/empty,\n * `String(value)` for unparseable input — never throws.\n *\n * @param {unknown} value A `Date`, parseable date string, or timestamp.\n * @return {string} The relative-day phrasing (or ''/original on bad input).\n */\nexport function formatDaysUntil(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst d = toDate(value)\n\tif (!d) {\n\t\treturn String(value)\n\t}\n\tconst days = dayDiffFromToday(d)\n\tif (days === 0) {\n\t\treturn t('nextcloud-vue', 'Due today')\n\t}\n\tif (days > 0) {\n\t\treturn n('nextcloud-vue', '{count} day remaining', '{count} days remaining', days, { count: days })\n\t}\n\tconst overdue = Math.abs(days)\n\treturn n('nextcloud-vue', '{count} day overdue', '{count} days overdue', overdue, { count: overdue })\n}\n\n/**\n * Elapsed-time formatter (`daysSince` registry key), i18n'd through the\n * library's own translation slug (`nextcloud-vue`) in the Nextcloud user\n * language. Whole calendar days, read as a running phrase so it sits after a\n * title or in a meta line: today renders \"today\", one day back renders\n * \"yesterday\", further back renders \"N days ago\". A (nonsensical but\n * harmless) future date renders \"tomorrow\" or \"in N days\". Day counts are\n * plural-aware via `translatePlural`.\n *\n * Null-safe per the built-in-formatter contract: `''` for null/empty,\n * `String(value)` for unparseable input, never throws.\n *\n * @param {unknown} value A `Date`, parseable date string, or timestamp.\n * @return {string} The relative-day phrasing (or ''/original on bad input).\n */\nexport function formatDaysSince(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst d = toDate(value)\n\tif (!d) {\n\t\treturn String(value)\n\t}\n\tconst days = dayDiffFromToday(d)\n\tif (days === 0) {\n\t\treturn t('nextcloud-vue', 'today')\n\t}\n\tif (days === -1) {\n\t\treturn t('nextcloud-vue', 'yesterday')\n\t}\n\tif (days === 1) {\n\t\treturn t('nextcloud-vue', 'tomorrow')\n\t}\n\tif (days < 0) {\n\t\tconst ago = Math.abs(days)\n\t\treturn n('nextcloud-vue', '{count} day ago', '{count} days ago', ago, { count: ago })\n\t}\n\treturn n('nextcloud-vue', 'in {count} day', 'in {count} days', days, { count: days })\n}\n\n/**\n * Relative-date formatter (`relativeDate` registry key): \"today\", \"yesterday\",\n * then the board's short date (\"5 okt\"). The column's `formatterOptions` may set\n * `time: true` to add the time of day after the day (\"vandaag, 09.42\").\n *\n * Null-safe per the built-in-formatter contract: `''` for null/empty,\n * `String(value)` for unparseable input, never throws.\n *\n * @param {unknown} value A `Date`, parseable date string, or timestamp.\n * @param {object} [_row] The full row (unused).\n * @param {object} [_property] The schema property (unused).\n * @param {{time?: boolean}} [options] The column's `formatterOptions`.\n * @return {string} The relative-day phrasing (or ''/original on bad input).\n * @spec openspec/changes/screens-relative-dates/specs/relative-dates/spec.md#requirement-a-relative-date-reads-today-yesterday-then-a-short-date\n */\nexport function formatRelativeDay(value, _row, _property, options) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst text = formatRelativeDate(value, { withTime: options?.time === true })\n\treturn text === '' ? String(value) : text\n}\n\n/**\n * Currency formatter (`currency` registry key) via `Intl.NumberFormat`.\n * Defaults to EUR; the column's `formatterOptions` may set `currency`\n * (ISO-4217 code, guarded by `safeCurrencyCode` so an invalid code can\n * never throw a `RangeError`) and `decimals` (default 2).\n *\n * Null-safe per the built-in-formatter contract: `''` for null/empty,\n * `String(value)` for non-numeric input — never throws.\n *\n * @param {unknown} value A numeric value (or numeric string).\n * @param {object} [row] The full row, for `currencyField`.\n * @param {object} [_property] The schema property (unused).\n * @param {{currency?: string, currencyField?: string, decimals?: number}} [options] The column's `formatterOptions`.\n * @return {string} The locale currency string (or ''/original on bad input).\n */\nexport function formatCurrency(value, row, _property, options) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn String(value)\n\t}\n\treturn formatMoney(num, options || {}, row)\n}\n\n/**\n * Sign/zero-based phrase selector (`conditionalPhrase` registry key) —\n * generalizes `daysUntil` to ANY numeric field: the column's\n * `formatterOptions` supply pre-translated `{ negative, zero, positive }`\n * phrases and the matching one renders with `{n}` replaced by the ABSOLUTE\n * value (so \"{n} days overdue\" reads naturally for -3). Phrases are\n * consumer-provided strings — apps translate them before declaring the\n * column, keeping i18n in the app layer.\n *\n * Null-safe per the built-in-formatter contract: `''` for null/empty,\n * `String(value)` for non-numeric input or when the selected phrase is\n * missing — never throws.\n *\n * @param {unknown} value A numeric value (or numeric string).\n * @param {object} [_row] The full row (unused).\n * @param {object} [_property] The schema property (unused).\n * @param {{negative?: string, zero?: string, positive?: string}} [options] The column's `formatterOptions`.\n * @return {string} The selected phrase with `{n}` substituted (or ''/original on bad input).\n */\nexport function formatConditionalPhrase(value, _row, _property, options) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn String(value)\n\t}\n\tconst opts = options || {}\n\tconst phrase = num < 0 ? opts.negative : (num > 0 ? opts.positive : opts.zero)\n\tif (typeof phrase !== 'string' || phrase === '') {\n\t\treturn String(value)\n\t}\n\treturn phrase.replace(/\\{n\\}/g, String(Math.abs(num)))\n}\n\n/**\n * Read a value that may be a collection SERIALISED as JSON back into the\n * collection itself. OpenRegister hands a stringified array back for some\n * property types (and an app's own import path may persist one), which\n * `formatCount` would otherwise report as a single entry.\n *\n * Only `[`/`{`-leading strings are tried, so an ordinary text value never pays\n * for a parse attempt, and a string that merely looks like JSON but isn't falls\n * back to being one entry.\n *\n * @param {unknown} value The raw cell value.\n * @return {unknown} The parsed collection, or `value` unchanged.\n */\nfunction parseCollection(value) {\n\tif (typeof value !== 'string') {\n\t\treturn value\n\t}\n\tconst trimmed = value.trim()\n\tif (trimmed[0] !== '[' && trimmed[0] !== '{') {\n\t\treturn value\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(trimmed)\n\t\treturn (parsed !== null && typeof parsed === 'object') ? parsed : value\n\t} catch {\n\t\treturn value\n\t}\n}\n\n/**\n * Entry-count summariser (`count` registry key) — renders a collection-valued\n * cell as \"5 frames\" instead of the truncated JSON blob `formatValue` would\n * produce. Counts array entries or object keys; a PRESENT scalar counts as 1.\n * The column's `formatterOptions` supply pre-translated\n * `{ singular, plural, zero }` phrases with `{n}` substituted, following the\n * `conditionalPhrase` convention of keeping i18n in the app layer. Without\n * phrases it renders the bare count.\n *\n * Null-safe per the built-in-formatter contract: `zero` (or `''`) for\n * null/empty — never throws.\n *\n * @param {unknown} value An array, object, JSON-encoded collection, or scalar.\n * @param {object} [_row] The full row (unused).\n * @param {object} [_property] The schema property (unused).\n * @param {{singular?: string, plural?: string, zero?: string}} [options] The column's `formatterOptions`.\n * @return {string} The selected phrase with `{n}` substituted, or the bare count.\n */\nexport function formatCount(value, _row, _property, options) {\n\tconst opts = options || {}\n\tconst zero = typeof opts.zero === 'string' ? opts.zero : ''\n\t// Every EMPTY value takes the zero phrase, not just null/''. `0` and `false`\n\t// used to fall through to the scalar branch and count as one entry, so a\n\t// column over a `0` rendered \"1 retry\" — the singular of a thing that isn't\n\t// there.\n\tif (value === null || value === undefined || value === '' || value === 0 || value === false) {\n\t\treturn zero\n\t}\n\tconst collection = parseCollection(value)\n\tlet n\n\tif (Array.isArray(collection)) {\n\t\tn = collection.length\n\t} else if (typeof collection === 'object') {\n\t\tn = Object.keys(collection).length\n\t} else {\n\t\tn = 1\n\t}\n\tif (n === 0) {\n\t\treturn zero\n\t}\n\tconst phrase = n === 1 ? (opts.singular ?? opts.plural) : (opts.plural ?? opts.singular)\n\tif (typeof phrase !== 'string' || phrase === '') {\n\t\treturn String(n)\n\t}\n\treturn phrase.replace(/\\{n\\}/g, String(n))\n}\n\n/**\n * The seven stored connection status values and their English source labels.\n *\n * The label is translated on each call, not here. A module-level `t()` runs\n * before the catalogue is registered and would freeze every label in English.\n */\nconst CONNECTION_STATUS_LABELS = {\n\tconfigured: 'Configured',\n\tlimited: 'Limited',\n\tunconfigured: 'Not configured',\n\tsimulated: 'Simulated',\n\tdisabled: 'Switched off',\n\tunavailable: 'Not available',\n\terror: 'Error',\n}\n\n/**\n * The short labels `formatterOptions.short` draws (board DqInstellingen\n * \"Aan / Uit / Fout\"): a connection that works in some form is on, one that\n * does not run is off, and a failing one is an error.\n */\nconst CONNECTION_STATUS_SHORT_LABELS = {\n\tconfigured: 'On',\n\tlimited: 'On',\n\tsimulated: 'On',\n\tunconfigured: 'Off',\n\tdisabled: 'Off',\n\tunavailable: 'Off',\n\terror: 'Error',\n}\n\n/**\n * Connection status formatter (`connectionStatus` registry key) for the\n * fleet connection registry. Renders one of the seven stored status values\n * as its translated label through the library's own slug (`nextcloud-vue`).\n * With `formatterOptions.short` it renders the board's three short labels\n * instead: On (configured, limited, simulated), Off (not configured,\n * switched off, not available) and Error.\n *\n * An unknown value passes through unchanged, so a status added later still\n * shows as its raw value instead of an empty cell. Null and undefined render\n * as `''`. Never throws.\n *\n * @param {unknown} value The stored status value.\n * @param {object} [row] The row (unused).\n * @param {object} [property] The column's property (unused).\n * @param {{short?: boolean}} [options] The column's `formatterOptions`.\n * @return {string} The translated label, or the value itself when it is not one of the seven.\n */\nexport function formatConnectionStatus(value, row, property, options) {\n\tif (value === null || value === undefined) {\n\t\treturn ''\n\t}\n\tconst labels = options && options.short === true ? CONNECTION_STATUS_SHORT_LABELS : CONNECTION_STATUS_LABELS\n\tconst label = Object.hasOwn(labels, value) ? labels[value] : null\n\treturn label ? t('nextcloud-vue', label) : String(value)\n}\n\n/**\n * Connection settings label formatter (`connectionSettingsLabel` registry\n * key). Renders \"Open settings\" when the row's `settingsUrl` is a non-empty\n * string, and `''` otherwise. Paired with the built-in `link` cell widget, an\n * empty label and href fall through to plain empty text, so a connection with\n * no settings section offers nothing to click.\n *\n * @param {unknown} value The row's `settingsUrl`.\n * @return {string} The translated link text, or `''`.\n */\nexport function formatConnectionSettingsLabel(value) {\n\treturn typeof value === 'string' && value !== ''\n\t\t? t('nextcloud-vue', 'Open settings')\n\t\t: ''\n}\n\n/**\n * Show a value with all but its last characters replaced by `*`, as the facts\n * widget's `masked` format does: an IBAN or a BSN reads `*****4567`.\n *\n * DISPLAY ONLY, NOT ACCESS CONTROL. The full value is still in the row the\n * page received; a reader with the browser's developer tools can read it.\n * A value the reader must not have belongs hidden in OpenRegister (property\n * authorization), not masked here.\n *\n * The column's `formatterOptions.visible` sets how many characters stay\n * (default 4); at most five `*` are drawn, so the length does not leak.\n * Null-safe: `''` for null or empty.\n *\n * @param {unknown} value The value.\n * @param {object} [_row] The full row (unused).\n * @param {object} [_property] The schema property (unused).\n * @param {{visible?: number}} [options] The column's `formatterOptions`.\n * @return {string} The masked value.\n * @spec openspec/changes/masked-formatter-max-aggregate/specs/index-page/spec.md#requirement-a-column-can-mask-its-value-for-display\n */\nexport function formatMasked(value, _row, _property, options) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst text = String(value)\n\tconst visible = Number.isInteger(options && options.visible) && options.visible >= 0 ? options.visible : 4\n\tif (text.length <= visible) {\n\t\treturn text\n\t}\n\treturn '*'.repeat(Math.min(text.length - visible, 5)) + (visible > 0 ? text.slice(-visible) : '')\n}\n\n/**\n * A stored value through a display map (`valueMap` registry key): the label\n * `options.map` gives the value, else the value as stored. \"Besloten\n * Vennootschap\" reads \"BV\" on DqOrganisaties.\n *\n * @param {unknown} value The stored value.\n * @param {object} [_row] The full row (unused).\n * @param {object} [_property] The schema property (unused).\n * @param {{map?: Object<string, string>}} [options] The column's `formatterOptions`.\n * @return {string} The label, or the value.\n * @spec openspec/changes/value-map-formatter/specs/index-page/spec.md#requirement-a-column-can-show-stored-values-through-a-display-map\n */\nexport function formatValueMap(value, _row, _property, options) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst map = options && options.map && typeof options.map === 'object' ? options.map : {}\n\tconst label = Object.hasOwn(map, String(value)) ? map[String(value)] : undefined\n\treturn typeof label === 'string' && label !== '' ? label : String(value)\n}\n\n/**\n * Built-in formatter registry merged under any consumer-registered\n * `formatters` in `CnAppRoot`'s `cnFormatters` provide.\n */\n/**\n * An ISO-8601 period of one unit in words (`isoDuration` registry key):\n * `P10Y` reads \"10 years\", `P6W` \"6 weeks\". A retention period or a term is\n * stored as such a period and was shown raw. Anything that is not a one-unit\n * period is returned as written, `''` for null or empty.\n *\n * @param {unknown} value The stored period.\n * @return {string} The period in words.\n */\nexport function formatIsoDuration(value) {\n\tif (value === null || value === undefined || value === '') {\n\t\treturn ''\n\t}\n\tconst period = parseDuration(String(value))\n\tif (period === null) {\n\t\treturn String(value)\n\t}\n\tconst words = {\n\t\tyears: () => n('nextcloud-vue', '%n year', '%n years', period.amount),\n\t\tmonths: () => n('nextcloud-vue', '%n month', '%n months', period.amount),\n\t\tweeks: () => n('nextcloud-vue', '%n week', '%n weeks', period.amount),\n\t\tdays: () => n('nextcloud-vue', '%n day', '%n days', period.amount),\n\t\thours: () => n('nextcloud-vue', '%n hour', '%n hours', period.amount),\n\t\tminutes: () => n('nextcloud-vue', '%n minute', '%n minutes', period.amount),\n\t}\n\treturn words[period.unit] ? words[period.unit]() : String(value)\n}\n\n/**\n * A Nextcloud user id as its display name (`userName` registry key): the id\n * while the name is looked up, and for a user nobody answers for.\n *\n * @spec openspec/changes/user-name-formatter/specs/user-name-formatter/spec.md#requirement-a-user-id-can-read-as-its-display-name\n * @param {unknown} value The user id.\n * @return {string} The display name, the id, or ''.\n */\nexport function formatUserName(value) {\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn ''\n\t}\n\treturn userDisplayName(value) || String(value)\n}\n\nexport const BUILT_IN_FORMATTERS = {\n\tdate: formatDate,\n\tdatetime: formatDateTime,\n\t'relative-time': formatRelativeTime,\n\tdaysSince: formatDaysSince,\n\tdaysUntil: formatDaysUntil,\n\trelativeDate: formatRelativeDay,\n\tcurrency: formatCurrency,\n\tconditionalPhrase: formatConditionalPhrase,\n\tcount: formatCount,\n\tconnectionStatus: formatConnectionStatus,\n\tconnectionSettingsLabel: formatConnectionSettingsLabel,\n\tmasked: formatMasked,\n\tisoDuration: formatIsoDuration,\n\tvalueMap: formatValueMap,\n\tuserName: formatUserName,\n}\n"],"names":["t","n"],"mappings":"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"}
@@ -177,10 +177,13 @@ function singleEqualityOf(filter) {
177
177
  * @param {Array<{key: (string|number), filter: object}>} options.entries The filters to count, each under its own key.
178
178
  * @param {object} [options.baseFilter] A filter every count is narrowed by (the page's fixed filter).
179
179
  * @param {object} [options.ctx] Token-resolution context for the filters.
180
+ * @param {string} [options.via] `'list'` counts every entry through the list endpoint, the request a list sends, so a
181
+ * count beside a list cannot disagree with it; else the aggregation endpoints where they apply.
180
182
  * @return {Promise<{[key: string]: number}>} The count per entry key. An entry whose count failed is left out.
181
183
  * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views
184
+ * @spec openspec/changes/index-counts-read-the-list-endpoint/specs/index-page/spec.md#requirement-an-index-pages-counts-come-from-the-list-request
182
185
  */
183
- async function fetchFilterCounts({ register, schema, entries, baseFilter, ctx } = {}) {
186
+ async function fetchFilterCounts({ register, schema, entries, baseFilter, ctx, via = '' } = {}) {
184
187
  const counts = {};
185
188
  if (!register || !schema || !Array.isArray(entries) || entries.length === 0) {
186
189
  return counts
@@ -192,7 +195,7 @@ async function fetchFilterCounts({ register, schema, entries, baseFilter, ctx }
192
195
  // and `/value` returns exactly its number.
193
196
  // A list of values anywhere in a request rules the aggregation endpoints
194
197
  // out for it. A base filter with one rules them out for every entry.
195
- const baseHasList = needsListEndpoint(base);
198
+ const baseHasList = via === 'list' || needsListEndpoint(base);
196
199
  const byField = {};
197
200
  const single = [];
198
201
  const viaList = [];
@@ -1 +1 @@
1
- {"version":3,"file":"fetchFilterCounts.js","sources":["../../../src/utils/fetchFilterCounts.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * fetchFilterCounts: how many records each of a set of filters matches, in as\n * few requests as the API allows.\n *\n * Quick filters and saved views usually differ in ONE field (`status: open`,\n * `status: closed`, ...). Those are answered by one `/grouped` request on that\n * field. A filter that narrows on something else (two fields, an operator, a\n * list of values) cannot be read off a grouped result, so each of those costs\n * one `/value` count. A filter that holds a LIST of values, or tests a field\n * for no value (`isnull`), is counted through the list endpoint, because the\n * aggregation endpoints cannot express it. So is a filter that spells its\n * operator in the key (`scheduledDate[gte]`): only the list endpoint reads it.\n * Nothing is requested for an entry that did not ask.\n *\n * @module utils/fetchFilterCounts\n */\n\nimport { fetchAggregateValue, fetchGroupedCounts } from './fetchAggregate.js'\nimport { buildHeaders, buildQueryString, prefixUrl } from './headers.js'\nimport { resolveFilterTokens } from './resolveFilterTokens.js'\n\n/**\n * Whether a filter holds a list of values (`{ status: ['open', 'hold'] }`).\n *\n * The aggregation endpoints have no spelling for \"one of these values\", so a\n * filter like this is never sent to them. It is counted through the list\n * endpoint instead, which is where the page's own request sends it.\n *\n * @param {object} filter A filter map.\n * @return {boolean} True when any value is an array.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport function hasListValue(filter) {\n\tif (!filter || typeof filter !== 'object') {\n\t\treturn false\n\t}\n\treturn Object.values(filter).some((value) => Array.isArray(value))\n}\n\n/**\n * Whether a filter tests a field for \"no value\" (`{ assignee: null }` or\n * `{ supersededBy: { isnull: true } }`).\n *\n * OpenRegister's aggregation endpoints have no null operator: `isnull` is\n * refused there and an empty value is dropped or matches nothing, so such a\n * filter counted 0 while the list showed rows. The list endpoint does read\n * `field[isnull]=true`, so a filter like this is counted through it.\n *\n * @param {object} filter A filter map.\n * @return {boolean} True when any value is null or carries an `isnull` operator.\n */\nexport function hasNullTest(filter) {\n\tif (!filter || typeof filter !== 'object') {\n\t\treturn false\n\t}\n\treturn Object.values(filter).some((value) => value === null\n\t\t|| (value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).some((op) => op.toLowerCase() === 'isnull')))\n}\n\n/**\n * Matches a filter key that carries its operator, `field[op]`.\n *\n * @type {RegExp}\n */\nconst OPERATOR_KEY = /\\[[A-Za-z_]+\\]$/\n\n/**\n * Whether a filter spells an operator in a KEY (`{ 'scheduledDate[gte]': '2026-10-13' }`).\n *\n * A lens on a date range needs this flat spelling, because the list request\n * would JSON-encode a nested `{ gte }` object. The aggregation endpoints do\n * not read it: they take the key as a field name, find no such field and\n * answer 0, so the lens read 0 while the list beside it showed rows. The list\n * endpoint reads the key exactly as the page's own request sends it.\n *\n * @param {object} filter A filter map.\n * @return {boolean} True when any key ends in `[op]`.\n * @spec openspec/changes/screens-operator-key-lens-counts/specs/index-page/spec.md\n */\nexport function hasOperatorKey(filter) {\n\tif (!filter || typeof filter !== 'object' || Array.isArray(filter)) {\n\t\treturn false\n\t}\n\treturn Object.keys(filter).some((key) => OPERATOR_KEY.test(key))\n}\n\n/**\n * Whether the aggregation endpoints cannot express a filter, so it is counted\n * through the list endpoint: a list of values, a test for no value, or an\n * operator spelled in a key.\n *\n * @param {object} filter A filter map.\n * @return {boolean} True when the filter needs the list endpoint.\n * @spec openspec/changes/screens-operator-key-lens-counts/specs/index-page/spec.md\n */\nexport function needsListEndpoint(filter) {\n\treturn hasListValue(filter) || hasNullTest(filter) || hasOperatorKey(filter)\n}\n\n/**\n * Count the records a filter matches by asking the LIST endpoint for one\n * record and reading its `total`. The filter is sent the way the list request\n * sends it: a list of values as repeated `field[]=`, an operator as\n * `field[op]=`.\n *\n * @param {string} register The register slug.\n * @param {string} schema The schema slug.\n * @param {object} filter The filter map.\n * @param {object} [ctx] Token-resolution context for the filter.\n * @return {Promise<number|null>} The total, or null when the response carries none.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport async function fetchListTotal(register, schema, filter, ctx) {\n\tconst resolved = resolveFilterTokens(filter || {}, ctx || {})\n\tconst params = {}\n\tfor (const [key, value] of Object.entries(resolved || {})) {\n\t\tif (value && typeof value === 'object' && !Array.isArray(value)) {\n\t\t\tfor (const [op, operand] of Object.entries(value)) {\n\t\t\t\tparams[`${key}[${op}]`] = operand\n\t\t\t}\n\t\t} else if (value === null) {\n\t\t\t// \"No value\": the list endpoint reads it as the isnull operator.\n\t\t\tparams[`${key}[isnull]`] = 'true'\n\t\t} else {\n\t\t\tparams[key] = value\n\t\t}\n\t}\n\tconst qs = buildQueryString({ ...params, _limit: 1 })\n\tconst url = prefixUrl(`/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}${qs}`)\n\tconst response = await fetch(url, { headers: buildHeaders() })\n\tif (!response.ok) {\n\t\tthrow new Error(`list returned ${response.status}`)\n\t}\n\tconst data = await response.json()\n\treturn typeof data.total === 'number' ? data.total : null\n}\n\n/**\n * The single field a filter narrows on with a plain equality, or null when the\n * filter is anything else, including an operator spelled in the key\n * (`scheduledDate[gte]`), which is a comparison and not a field name.\n *\n * @param {object} filter A filter map.\n * @return {{field: string, value: string}|null} The field and the value it must equal.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport function singleEqualityOf(filter) {\n\tif (!filter || typeof filter !== 'object' || Array.isArray(filter)) {\n\t\treturn null\n\t}\n\tconst keys = Object.keys(filter)\n\tif (keys.length !== 1 || OPERATOR_KEY.test(keys[0])) {\n\t\treturn null\n\t}\n\tconst value = filter[keys[0]]\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn null\n\t}\n\t// A token (`@me`, `@today`) resolves to a value the grouped keys are\n\t// compared against only after resolution, which the count request does.\n\tif (typeof value === 'string' && value.charAt(0) === '@') {\n\t\treturn null\n\t}\n\treturn { field: keys[0], value: String(value) }\n}\n\n/**\n * Count the records each entry's filter matches.\n *\n * @param {object} options The request.\n * @param {string} options.register The register slug.\n * @param {string} options.schema The schema slug.\n * @param {Array<{key: (string|number), filter: object}>} options.entries The filters to count, each under its own key.\n * @param {object} [options.baseFilter] A filter every count is narrowed by (the page's fixed filter).\n * @param {object} [options.ctx] Token-resolution context for the filters.\n * @return {Promise<{[key: string]: number}>} The count per entry key. An entry whose count failed is left out.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport async function fetchFilterCounts({ register, schema, entries, baseFilter, ctx } = {}) {\n\tconst counts = {}\n\tif (!register || !schema || !Array.isArray(entries) || entries.length === 0) {\n\t\treturn counts\n\t}\n\tconst base = (baseFilter && typeof baseFilter === 'object') ? baseFilter : {}\n\n\t// Entries that share one equality field go into one grouped request, but\n\t// only when at least two do: a lone entry costs one request either way,\n\t// and `/value` returns exactly its number.\n\t// A list of values anywhere in a request rules the aggregation endpoints\n\t// out for it. A base filter with one rules them out for every entry.\n\tconst baseHasList = needsListEndpoint(base)\n\tconst byField = {}\n\tconst single = []\n\tconst viaList = []\n\tfor (const entry of entries) {\n\t\tif (baseHasList || needsListEndpoint(entry.filter)) {\n\t\t\tviaList.push(entry)\n\t\t\tcontinue\n\t\t}\n\t\tconst eq = singleEqualityOf(entry.filter)\n\t\tif (eq !== null && !(eq.field in base)) {\n\t\t\t(byField[eq.field] = byField[eq.field] || []).push({ entry, value: eq.value })\n\t\t} else {\n\t\t\tsingle.push(entry)\n\t\t}\n\t}\n\tconst jobs = []\n\tfor (const [field, members] of Object.entries(byField)) {\n\t\tif (members.length < 2) {\n\t\t\tsingle.push(members[0].entry)\n\t\t\tcontinue\n\t\t}\n\t\tjobs.push(fetchGroupedCounts({ register, schema, groupBy: field, filter: base }, ctx)\n\t\t\t.then((groups) => {\n\t\t\t\tconst map = {}\n\t\t\t\tfor (const group of groups) {\n\t\t\t\t\tmap[group.key] = group.count\n\t\t\t\t}\n\t\t\t\tfor (const member of members) {\n\t\t\t\t\tcounts[member.entry.key] = map[member.value] || 0\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}))\n\t}\n\tfor (const entry of single) {\n\t\tjobs.push(fetchAggregateValue({ register, schema, metric: 'count', filter: { ...base, ...(entry.filter || {}) } }, ctx)\n\t\t\t.then((value) => {\n\t\t\t\tif (value !== null && Number.isFinite(Number(value))) {\n\t\t\t\t\tcounts[entry.key] = Number(value)\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}))\n\t}\n\tfor (const entry of viaList) {\n\t\tjobs.push(fetchListTotal(register, schema, { ...base, ...(entry.filter || {}) }, ctx)\n\t\t\t.then((total) => {\n\t\t\t\tif (total !== null) {\n\t\t\t\t\tcounts[entry.key] = total\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}))\n\t}\n\tawait Promise.all(jobs)\n\treturn counts\n}\n"],"names":[],"mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,YAAY,CAAC,MAAM,EAAE;AACrC,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AAC5C,EAAE,OAAO,KAAK;AACd,EAAE;AACF,CAAC,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,WAAW,CAAC,MAAM,EAAE;AACpC,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AAC5C,EAAE,OAAO,KAAK;AACd,EAAE;AACF,CAAC,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,KAAK,KAAK,IAAI;AAC5D,MAAM,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,WAAW,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC;AACrI,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,YAAY,GAAG,kBAAiB;AACtC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,cAAc,CAAC,MAAM,EAAE;AACvC,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;AACrE,EAAE,OAAO,KAAK;AACd,EAAE;AACF,CAAC,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACjE,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,iBAAiB,CAAC,MAAM,EAAE;AAC1C,CAAC,OAAO,YAAY,CAAC,MAAM,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,IAAI,cAAc,CAAC,MAAM,CAAC;AAC7E,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,eAAe,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE;AACpE,CAAC,MAAM,QAAQ,GAAG,mBAAmB,CAAC,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,EAAC;AAC9D,CAAC,MAAM,MAAM,GAAG,GAAE;AAClB,CAAC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE;AAC5D,EAAE,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;AACnE,GAAG,KAAK,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;AACtD,IAAI,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,QAAO;AACrC,IAAI;AACJ,GAAG,MAAM,IAAI,KAAK,KAAK,IAAI,EAAE;AAC7B;AACA,GAAG,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,OAAM;AACpC,GAAG,MAAM;AACT,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,MAAK;AACtB,GAAG;AACH,EAAE;AACF,CAAC,MAAM,EAAE,GAAG,gBAAgB,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,EAAC;AACtD,CAAC,MAAM,GAAG,GAAG,SAAS,CAAC,CAAC,+BAA+B,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAC;AAC3H,CAAC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAC;AAC/D,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE;AACnB,EAAE,MAAM,IAAI,KAAK,CAAC,CAAC,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AACrD,EAAE;AACF,CAAC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,GAAE;AACnC,CAAC,OAAO,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI;AAC1D,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,gBAAgB,CAAC,MAAM,EAAE;AACzC,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;AACrE,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAC;AACjC,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE;AACtD,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAC;AAC9B,CAAC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;AACzF,EAAE,OAAO,IAAI;AACb,EAAE;AACF;AACA;AACA,CAAC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE;AAC3D,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE;AAChD,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,eAAe,iBAAiB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;AAC7F,CAAC,MAAM,MAAM,GAAG,GAAE;AAClB,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE;AAC9E,EAAE,OAAO,MAAM;AACf,EAAE;AACF,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,GAAG,GAAE;AAC9E;AACA;AACA;AACA;AACA;AACA;AACA,CAAC,MAAM,WAAW,GAAG,iBAAiB,CAAC,IAAI,EAAC;AAC5C,CAAC,MAAM,OAAO,GAAG,GAAE;AACnB,CAAC,MAAM,MAAM,GAAG,GAAE;AAClB,CAAC,MAAM,OAAO,GAAG,GAAE;AACnB,CAAC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE;AAC9B,EAAE,IAAI,WAAW,IAAI,iBAAiB,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE;AACtD,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,EAAC;AACtB,GAAG,QAAQ;AACX,GAAG;AACH,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAAC,KAAK,CAAC,MAAM,EAAC;AAC3C,EAAE,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,IAAI,CAAC,EAAE;AAC1C,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,EAAC;AACjF,GAAG,MAAM;AACT,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAC;AACrB,GAAG;AACH,EAAE;AACF,CAAC,MAAM,IAAI,GAAG,GAAE;AAChB,CAAC,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;AACzD,EAAE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE;AAC1B,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,EAAC;AAChC,GAAG,QAAQ;AACX,GAAG;AACH,EAAE,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,GAAG,CAAC;AACvF,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK;AACrB,IAAI,MAAM,GAAG,GAAG,GAAE;AAClB,IAAI,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE;AAChC,KAAK,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAK;AACjC,KAAK;AACL,IAAI,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE;AAClC,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAC;AACtD,KAAK;AACL,IAAI,CAAC;AACL,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC,EAAC;AACpB,EAAE;AACF,CAAC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE;AAC7B,EAAE,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC;AACzH,IAAI,IAAI,CAAC,CAAC,KAAK,KAAK;AACpB,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE;AAC1D,KAAK,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,EAAC;AACtC,KAAK;AACL,IAAI,CAAC;AACL,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC,EAAC;AACpB,EAAE;AACF,CAAC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE;AAC9B,EAAE,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC;AACvF,IAAI,IAAI,CAAC,CAAC,KAAK,KAAK;AACpB,IAAI,IAAI,KAAK,KAAK,IAAI,EAAE;AACxB,KAAK,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,MAAK;AAC9B,KAAK;AACL,IAAI,CAAC;AACL,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC,EAAC;AACpB,EAAE;AACF,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,EAAC;AACxB,CAAC,OAAO,MAAM;AACd;;;;"}
1
+ {"version":3,"file":"fetchFilterCounts.js","sources":["../../../src/utils/fetchFilterCounts.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * fetchFilterCounts: how many records each of a set of filters matches, in as\n * few requests as the API allows.\n *\n * Quick filters and saved views usually differ in ONE field (`status: open`,\n * `status: closed`, ...). Those are answered by one `/grouped` request on that\n * field. A filter that narrows on something else (two fields, an operator, a\n * list of values) cannot be read off a grouped result, so each of those costs\n * one `/value` count. A filter that holds a LIST of values, or tests a field\n * for no value (`isnull`), is counted through the list endpoint, because the\n * aggregation endpoints cannot express it. So is a filter that spells its\n * operator in the key (`scheduledDate[gte]`): only the list endpoint reads it.\n * Nothing is requested for an entry that did not ask.\n *\n * @module utils/fetchFilterCounts\n */\n\nimport { fetchAggregateValue, fetchGroupedCounts } from './fetchAggregate.js'\nimport { buildHeaders, buildQueryString, prefixUrl } from './headers.js'\nimport { resolveFilterTokens } from './resolveFilterTokens.js'\n\n/**\n * Whether a filter holds a list of values (`{ status: ['open', 'hold'] }`).\n *\n * The aggregation endpoints have no spelling for \"one of these values\", so a\n * filter like this is never sent to them. It is counted through the list\n * endpoint instead, which is where the page's own request sends it.\n *\n * @param {object} filter A filter map.\n * @return {boolean} True when any value is an array.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport function hasListValue(filter) {\n\tif (!filter || typeof filter !== 'object') {\n\t\treturn false\n\t}\n\treturn Object.values(filter).some((value) => Array.isArray(value))\n}\n\n/**\n * Whether a filter tests a field for \"no value\" (`{ assignee: null }` or\n * `{ supersededBy: { isnull: true } }`).\n *\n * OpenRegister's aggregation endpoints have no null operator: `isnull` is\n * refused there and an empty value is dropped or matches nothing, so such a\n * filter counted 0 while the list showed rows. The list endpoint does read\n * `field[isnull]=true`, so a filter like this is counted through it.\n *\n * @param {object} filter A filter map.\n * @return {boolean} True when any value is null or carries an `isnull` operator.\n */\nexport function hasNullTest(filter) {\n\tif (!filter || typeof filter !== 'object') {\n\t\treturn false\n\t}\n\treturn Object.values(filter).some((value) => value === null\n\t\t|| (value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).some((op) => op.toLowerCase() === 'isnull')))\n}\n\n/**\n * Matches a filter key that carries its operator, `field[op]`.\n *\n * @type {RegExp}\n */\nconst OPERATOR_KEY = /\\[[A-Za-z_]+\\]$/\n\n/**\n * Whether a filter spells an operator in a KEY (`{ 'scheduledDate[gte]': '2026-10-13' }`).\n *\n * A lens on a date range needs this flat spelling, because the list request\n * would JSON-encode a nested `{ gte }` object. The aggregation endpoints do\n * not read it: they take the key as a field name, find no such field and\n * answer 0, so the lens read 0 while the list beside it showed rows. The list\n * endpoint reads the key exactly as the page's own request sends it.\n *\n * @param {object} filter A filter map.\n * @return {boolean} True when any key ends in `[op]`.\n * @spec openspec/changes/screens-operator-key-lens-counts/specs/index-page/spec.md\n */\nexport function hasOperatorKey(filter) {\n\tif (!filter || typeof filter !== 'object' || Array.isArray(filter)) {\n\t\treturn false\n\t}\n\treturn Object.keys(filter).some((key) => OPERATOR_KEY.test(key))\n}\n\n/**\n * Whether the aggregation endpoints cannot express a filter, so it is counted\n * through the list endpoint: a list of values, a test for no value, or an\n * operator spelled in a key.\n *\n * @param {object} filter A filter map.\n * @return {boolean} True when the filter needs the list endpoint.\n * @spec openspec/changes/screens-operator-key-lens-counts/specs/index-page/spec.md\n */\nexport function needsListEndpoint(filter) {\n\treturn hasListValue(filter) || hasNullTest(filter) || hasOperatorKey(filter)\n}\n\n/**\n * Count the records a filter matches by asking the LIST endpoint for one\n * record and reading its `total`. The filter is sent the way the list request\n * sends it: a list of values as repeated `field[]=`, an operator as\n * `field[op]=`.\n *\n * @param {string} register The register slug.\n * @param {string} schema The schema slug.\n * @param {object} filter The filter map.\n * @param {object} [ctx] Token-resolution context for the filter.\n * @return {Promise<number|null>} The total, or null when the response carries none.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport async function fetchListTotal(register, schema, filter, ctx) {\n\tconst resolved = resolveFilterTokens(filter || {}, ctx || {})\n\tconst params = {}\n\tfor (const [key, value] of Object.entries(resolved || {})) {\n\t\tif (value && typeof value === 'object' && !Array.isArray(value)) {\n\t\t\tfor (const [op, operand] of Object.entries(value)) {\n\t\t\t\tparams[`${key}[${op}]`] = operand\n\t\t\t}\n\t\t} else if (value === null) {\n\t\t\t// \"No value\": the list endpoint reads it as the isnull operator.\n\t\t\tparams[`${key}[isnull]`] = 'true'\n\t\t} else {\n\t\t\tparams[key] = value\n\t\t}\n\t}\n\tconst qs = buildQueryString({ ...params, _limit: 1 })\n\tconst url = prefixUrl(`/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}${qs}`)\n\tconst response = await fetch(url, { headers: buildHeaders() })\n\tif (!response.ok) {\n\t\tthrow new Error(`list returned ${response.status}`)\n\t}\n\tconst data = await response.json()\n\treturn typeof data.total === 'number' ? data.total : null\n}\n\n/**\n * The single field a filter narrows on with a plain equality, or null when the\n * filter is anything else, including an operator spelled in the key\n * (`scheduledDate[gte]`), which is a comparison and not a field name.\n *\n * @param {object} filter A filter map.\n * @return {{field: string, value: string}|null} The field and the value it must equal.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n */\nexport function singleEqualityOf(filter) {\n\tif (!filter || typeof filter !== 'object' || Array.isArray(filter)) {\n\t\treturn null\n\t}\n\tconst keys = Object.keys(filter)\n\tif (keys.length !== 1 || OPERATOR_KEY.test(keys[0])) {\n\t\treturn null\n\t}\n\tconst value = filter[keys[0]]\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn null\n\t}\n\t// A token (`@me`, `@today`) resolves to a value the grouped keys are\n\t// compared against only after resolution, which the count request does.\n\tif (typeof value === 'string' && value.charAt(0) === '@') {\n\t\treturn null\n\t}\n\treturn { field: keys[0], value: String(value) }\n}\n\n/**\n * Count the records each entry's filter matches.\n *\n * @param {object} options The request.\n * @param {string} options.register The register slug.\n * @param {string} options.schema The schema slug.\n * @param {Array<{key: (string|number), filter: object}>} options.entries The filters to count, each under its own key.\n * @param {object} [options.baseFilter] A filter every count is narrowed by (the page's fixed filter).\n * @param {object} [options.ctx] Token-resolution context for the filters.\n * @param {string} [options.via] `'list'` counts every entry through the list endpoint, the request a list sends, so a\n * count beside a list cannot disagree with it; else the aggregation endpoints where they apply.\n * @return {Promise<{[key: string]: number}>} The count per entry key. An entry whose count failed is left out.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views\n * @spec openspec/changes/index-counts-read-the-list-endpoint/specs/index-page/spec.md#requirement-an-index-pages-counts-come-from-the-list-request\n */\nexport async function fetchFilterCounts({ register, schema, entries, baseFilter, ctx, via = '' } = {}) {\n\tconst counts = {}\n\tif (!register || !schema || !Array.isArray(entries) || entries.length === 0) {\n\t\treturn counts\n\t}\n\tconst base = (baseFilter && typeof baseFilter === 'object') ? baseFilter : {}\n\n\t// Entries that share one equality field go into one grouped request, but\n\t// only when at least two do: a lone entry costs one request either way,\n\t// and `/value` returns exactly its number.\n\t// A list of values anywhere in a request rules the aggregation endpoints\n\t// out for it. A base filter with one rules them out for every entry.\n\tconst baseHasList = via === 'list' || needsListEndpoint(base)\n\tconst byField = {}\n\tconst single = []\n\tconst viaList = []\n\tfor (const entry of entries) {\n\t\tif (baseHasList || needsListEndpoint(entry.filter)) {\n\t\t\tviaList.push(entry)\n\t\t\tcontinue\n\t\t}\n\t\tconst eq = singleEqualityOf(entry.filter)\n\t\tif (eq !== null && !(eq.field in base)) {\n\t\t\t(byField[eq.field] = byField[eq.field] || []).push({ entry, value: eq.value })\n\t\t} else {\n\t\t\tsingle.push(entry)\n\t\t}\n\t}\n\tconst jobs = []\n\tfor (const [field, members] of Object.entries(byField)) {\n\t\tif (members.length < 2) {\n\t\t\tsingle.push(members[0].entry)\n\t\t\tcontinue\n\t\t}\n\t\tjobs.push(fetchGroupedCounts({ register, schema, groupBy: field, filter: base }, ctx)\n\t\t\t.then((groups) => {\n\t\t\t\tconst map = {}\n\t\t\t\tfor (const group of groups) {\n\t\t\t\t\tmap[group.key] = group.count\n\t\t\t\t}\n\t\t\t\tfor (const member of members) {\n\t\t\t\t\tcounts[member.entry.key] = map[member.value] || 0\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}))\n\t}\n\tfor (const entry of single) {\n\t\tjobs.push(fetchAggregateValue({ register, schema, metric: 'count', filter: { ...base, ...(entry.filter || {}) } }, ctx)\n\t\t\t.then((value) => {\n\t\t\t\tif (value !== null && Number.isFinite(Number(value))) {\n\t\t\t\t\tcounts[entry.key] = Number(value)\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}))\n\t}\n\tfor (const entry of viaList) {\n\t\tjobs.push(fetchListTotal(register, schema, { ...base, ...(entry.filter || {}) }, ctx)\n\t\t\t.then((total) => {\n\t\t\t\tif (total !== null) {\n\t\t\t\t\tcounts[entry.key] = total\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {}))\n\t}\n\tawait Promise.all(jobs)\n\treturn counts\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ import { CONTAINS_OPERATOR } from './columnFilters.js';
2
+
3
+ /**
4
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
5
+ * SPDX-License-Identifier: EUPL-1.2
6
+ *
7
+ * Header sort and filter applied to rows the table already holds (an
8
+ * `endpointSource` table): the same `{ field, dir }` sort and
9
+ * `{ paramKey: values[] }` filter map a self-fetching table sends to the
10
+ * server, read here in the browser.
11
+ *
12
+ * @module utils/localHeaderRows
13
+ */
14
+
15
+
16
+ /**
17
+ * A row value as one comparable scalar: an array counts as its joined items.
18
+ *
19
+ * @param {unknown} value The raw value.
20
+ * @return {string|number|null} The value to compare.
21
+ */
22
+ function scalar(value) {
23
+ if (value === null || value === undefined) {
24
+ return null
25
+ }
26
+ if (Array.isArray(value)) {
27
+ return value.map((item) => (item && typeof item === 'object' ? (item.title || item.name || '') : String(item))).join(', ')
28
+ }
29
+ if (typeof value === 'number' || typeof value === 'boolean') {
30
+ return typeof value === 'boolean' ? String(value) : value
31
+ }
32
+ return typeof value === 'object' ? (value.title || value.name || '') : String(value)
33
+ }
34
+
35
+ /**
36
+ * Whether a row holds a value the filter entry asks for.
37
+ *
38
+ * @param {object} row The row.
39
+ * @param {string} param The filter key (`field`, `field[like]`, `field[gte]`, `field[lte]`).
40
+ * @param {Array<string>} values The chosen values.
41
+ * @return {boolean} True when the row passes.
42
+ */
43
+ function passes(row, param, values) {
44
+ const match = /^(.+)\[(\w+)\]$/.exec(param);
45
+ const field = match ? match[1] : param;
46
+ const op = match ? match[2] : '';
47
+ const value = scalar(row[field]);
48
+ if (op === CONTAINS_OPERATOR) {
49
+ const term = String(values[0]).toLowerCase();
50
+ return value !== null && String(value).toLowerCase().includes(term)
51
+ }
52
+ if (op === 'gte' || op === 'lte') {
53
+ if (value === null) {
54
+ return false
55
+ }
56
+ const left = typeof value === 'number' ? value : String(value);
57
+ const right = typeof value === 'number' ? Number(values[0]) : String(values[0]);
58
+ return op === 'gte' ? left >= right : left <= right
59
+ }
60
+ return value !== null && values.map(String).includes(String(value))
61
+ }
62
+
63
+ /**
64
+ * The rows that pass every active header filter, in the header sort's order.
65
+ *
66
+ * @param {Array<object>} rows The rows.
67
+ * @param {{field: string, dir: string}|null} sort The header sort.
68
+ * @param {object} filters The header filters, `{ paramKey: values[] }`.
69
+ * @return {Array<object>} The rows to show (the same array when nothing applies).
70
+ */
71
+ function localHeaderRows(rows, sort, filters) {
72
+ if (!Array.isArray(rows)) {
73
+ return []
74
+ }
75
+ const active = Object.entries(filters || {}).filter(([, values]) => Array.isArray(values) && values.length > 0);
76
+ let out = active.length > 0 ? rows.filter((row) => row && active.every(([param, values]) => passes(row, param, values))) : rows;
77
+ if (sort && sort.field) {
78
+ const sign = sort.dir === 'desc' ? -1 : 1;
79
+ out = [...out].sort((a, b) => {
80
+ const left = scalar(a && a[sort.field]);
81
+ const right = scalar(b && b[sort.field]);
82
+ if (left === null || right === null) {
83
+ return left === right ? 0 : (left === null ? 1 : -1)
84
+ }
85
+ if (typeof left === 'number' && typeof right === 'number') {
86
+ return sign * (left - right)
87
+ }
88
+ return sign * String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' })
89
+ });
90
+ }
91
+ return out
92
+ }
93
+
94
+ export { localHeaderRows };
95
+ //# sourceMappingURL=localHeaderRows.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localHeaderRows.js","sources":["../../../src/utils/localHeaderRows.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Header sort and filter applied to rows the table already holds (an\n * `endpointSource` table): the same `{ field, dir }` sort and\n * `{ paramKey: values[] }` filter map a self-fetching table sends to the\n * server, read here in the browser.\n *\n * @module utils/localHeaderRows\n */\n\nimport { CONTAINS_OPERATOR } from './columnFilters.js'\n\n/**\n * A row value as one comparable scalar: an array counts as its joined items.\n *\n * @param {unknown} value The raw value.\n * @return {string|number|null} The value to compare.\n */\nfunction scalar(value) {\n\tif (value === null || value === undefined) {\n\t\treturn null\n\t}\n\tif (Array.isArray(value)) {\n\t\treturn value.map((item) => (item && typeof item === 'object' ? (item.title || item.name || '') : String(item))).join(', ')\n\t}\n\tif (typeof value === 'number' || typeof value === 'boolean') {\n\t\treturn typeof value === 'boolean' ? String(value) : value\n\t}\n\treturn typeof value === 'object' ? (value.title || value.name || '') : String(value)\n}\n\n/**\n * Whether a row holds a value the filter entry asks for.\n *\n * @param {object} row The row.\n * @param {string} param The filter key (`field`, `field[like]`, `field[gte]`, `field[lte]`).\n * @param {Array<string>} values The chosen values.\n * @return {boolean} True when the row passes.\n */\nfunction passes(row, param, values) {\n\tconst match = /^(.+)\\[(\\w+)\\]$/.exec(param)\n\tconst field = match ? match[1] : param\n\tconst op = match ? match[2] : ''\n\tconst value = scalar(row[field])\n\tif (op === CONTAINS_OPERATOR) {\n\t\tconst term = String(values[0]).toLowerCase()\n\t\treturn value !== null && String(value).toLowerCase().includes(term)\n\t}\n\tif (op === 'gte' || op === 'lte') {\n\t\tif (value === null) {\n\t\t\treturn false\n\t\t}\n\t\tconst left = typeof value === 'number' ? value : String(value)\n\t\tconst right = typeof value === 'number' ? Number(values[0]) : String(values[0])\n\t\treturn op === 'gte' ? left >= right : left <= right\n\t}\n\treturn value !== null && values.map(String).includes(String(value))\n}\n\n/**\n * The rows that pass every active header filter, in the header sort's order.\n *\n * @param {Array<object>} rows The rows.\n * @param {{field: string, dir: string}|null} sort The header sort.\n * @param {object} filters The header filters, `{ paramKey: values[] }`.\n * @return {Array<object>} The rows to show (the same array when nothing applies).\n */\nexport function localHeaderRows(rows, sort, filters) {\n\tif (!Array.isArray(rows)) {\n\t\treturn []\n\t}\n\tconst active = Object.entries(filters || {}).filter(([, values]) => Array.isArray(values) && values.length > 0)\n\tlet out = active.length > 0 ? rows.filter((row) => row && active.every(([param, values]) => passes(row, param, values))) : rows\n\tif (sort && sort.field) {\n\t\tconst sign = sort.dir === 'desc' ? -1 : 1\n\t\tout = [...out].sort((a, b) => {\n\t\t\tconst left = scalar(a && a[sort.field])\n\t\t\tconst right = scalar(b && b[sort.field])\n\t\t\tif (left === null || right === null) {\n\t\t\t\treturn left === right ? 0 : (left === null ? 1 : -1)\n\t\t\t}\n\t\t\tif (typeof left === 'number' && typeof right === 'number') {\n\t\t\t\treturn sign * (left - right)\n\t\t\t}\n\t\t\treturn sign * String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' })\n\t\t})\n\t}\n\treturn out\n}\n"],"names":[],"mappings":"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"}
@@ -27,20 +27,33 @@ function ocsData(res) {
27
27
  return (res && res.data && res.data.ocs && res.data.ocs.data) || null
28
28
  }
29
29
 
30
+ /** The most groups read, so a server that ignores the offset cannot loop forever. */
31
+ const MAX_GROUPS = 5000;
32
+
30
33
  /**
31
34
  * The groups, with their member counts.
32
35
  *
33
36
  * @param {object} axios The axios instance.
34
- * @param {{search?: string, limit?: number}} [options] Narrowing.
37
+ * @param {{search?: string, limit?: number}} [options] Narrowing; `limit` is the page size.
35
38
  * @return {Promise<Array<{id: string, name: string, count: number}>>}
36
39
  * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members
37
40
  */
38
41
  async function fetchGroups(axios, options = {}) {
39
- const res = await axios.get(generateOcsUrl('cloud/groups/details'), {
40
- params: { search: options.search || '', limit: options.limit || 200, format: 'json' },
41
- headers: OCS_HEADERS,
42
- });
43
- const groups = (ocsData(res) && ocsData(res).groups) || [];
42
+ const limit = options.limit || 200;
43
+ // Every page, while a page comes back full: one page cut an instance with
44
+ // more groups than the limit off before its last ones (DqAfdelingen).
45
+ const groups = [];
46
+ for (let offset = 0; offset < MAX_GROUPS; offset += limit) {
47
+ const res = await axios.get(generateOcsUrl('cloud/groups/details'), {
48
+ params: { search: options.search || '', limit, offset, format: 'json' },
49
+ headers: OCS_HEADERS,
50
+ });
51
+ const batch = (ocsData(res) && ocsData(res).groups) || [];
52
+ groups.push(...batch);
53
+ if (batch.length < limit) {
54
+ break
55
+ }
56
+ }
44
57
  return groups.map((group) => ({
45
58
  id: String(group.id),
46
59
  name: group.displayname || String(group.id),
@@ -1 +1 @@
1
- {"version":3,"file":"nextcloudDirectory.js","sources":["../../../src/utils/nextcloudDirectory.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n */\n\nimport { generateOcsUrl } from '@nextcloud/router'\n\n/**\n * Nextcloud's own directory: groups, the users in a group, and whether a\n * user is in today. Read through the OCS API with the reader's session, so\n * Nextcloud decides what the reader may see (listing a group's members needs\n * an admin or the group's sub-admin).\n *\n * @module utils/nextcloudDirectory\n */\n\nconst OCS_HEADERS = { 'OCS-APIRequest': 'true' }\n\n/**\n * The `ocs.data` of an OCS response, or null.\n *\n * @param {object} res An axios response.\n * @return {object|null}\n */\nfunction ocsData(res) {\n\treturn (res && res.data && res.data.ocs && res.data.ocs.data) || null\n}\n\n/**\n * The groups, with their member counts.\n *\n * @param {object} axios The axios instance.\n * @param {{search?: string, limit?: number}} [options] Narrowing.\n * @return {Promise<Array<{id: string, name: string, count: number}>>}\n * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members\n */\nexport async function fetchGroups(axios, options = {}) {\n\tconst res = await axios.get(generateOcsUrl('cloud/groups/details'), {\n\t\tparams: { search: options.search || '', limit: options.limit || 200, format: 'json' },\n\t\theaders: OCS_HEADERS,\n\t})\n\tconst groups = (ocsData(res) && ocsData(res).groups) || []\n\treturn groups.map((group) => ({\n\t\tid: String(group.id),\n\t\tname: group.displayname || String(group.id),\n\t\tcount: Number(group.usercount) || 0,\n\t}))\n}\n\n/**\n * The members of one group, with their details.\n *\n * @param {object} axios The axios instance.\n * @param {string} groupId The group id.\n * @return {Promise<Array<object>>} Nextcloud's user details, one per member.\n * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members\n */\nexport async function fetchGroupMembers(axios, groupId) {\n\tconst res = await axios.get(generateOcsUrl('cloud/groups/{groupId}/users/details', { groupId }), {\n\t\tparams: { format: 'json' },\n\t\theaders: OCS_HEADERS,\n\t})\n\tconst users = (ocsData(res) && ocsData(res).users) || {}\n\treturn Array.isArray(users) ? users : Object.values(users)\n}\n\n/**\n * Every user the reader may list, with their details: all users for an\n * admin, the users of their groups for a sub-admin. Refused for others.\n *\n * @param {object} axios The axios instance.\n * @param {{limit?: number}} [options] How many to read.\n * @return {Promise<Array<object>>} Nextcloud's user details, one per user.\n */\nexport async function fetchUsers(axios, options = {}) {\n\tconst res = await axios.get(generateOcsUrl('cloud/users/details'), {\n\t\tparams: { limit: options.limit || 500, format: 'json' },\n\t\theaders: OCS_HEADERS,\n\t})\n\tconst users = (ocsData(res) && ocsData(res).users) || {}\n\treturn Array.isArray(users) ? users : Object.values(users)\n}\n\n/**\n * Whether a user is in today: an out-of-office period that runs now (the\n * dav app's absence, Nextcloud 28 and later), else their user status.\n *\n * @param {object} axios The axios instance.\n * @param {string} userId The user id.\n * @param {object|null} status The user's entry from the user status list, or null.\n * @return {Promise<{state: string, until: (string|null), message: string}>}\n * `state` is `absent` (with `until`, an ISO date), `away`, `present` or `unknown`.\n * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members\n */\nexport async function fetchPresence(axios, userId, status) {\n\ttry {\n\t\tconst res = await axios.get(generateOcsUrl('apps/dav/api/v1/outOfOffice/{userId}/now', { userId }), { params: { format: 'json' }, headers: OCS_HEADERS })\n\t\tconst ooo = ocsData(res)\n\t\tif (ooo && ooo.endDate) {\n\t\t\tconst end = typeof ooo.endDate === 'number' ? new Date(ooo.endDate * 1000) : new Date(ooo.endDate)\n\t\t\treturn { state: 'absent', until: Number.isNaN(end.getTime()) ? null : end.toISOString().slice(0, 10), message: ooo.shortMessage || '' }\n\t\t}\n\t} catch {\n\t\t// 404: no absence now, or an instance without the absence API.\n\t}\n\tif (status && typeof status === 'object') {\n\t\tconst message = typeof status.message === 'string' ? status.message : ''\n\t\tif (status.status === 'online') {\n\t\t\treturn { state: 'present', until: null, message }\n\t\t}\n\t\tif (status.status === 'away' || status.status === 'dnd') {\n\t\t\treturn { state: 'away', until: null, message }\n\t\t}\n\t}\n\treturn { state: 'unknown', until: null, message: '' }\n}\n\n/**\n * The user statuses Nextcloud shares with the reader, by user id. Empty when\n * the user status app is off.\n *\n * @param {object} axios The axios instance.\n * @return {Promise<Record<string, object>>}\n * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members\n */\nexport async function fetchStatuses(axios) {\n\ttry {\n\t\tconst res = await axios.get(generateOcsUrl('apps/user_status/api/v1/statuses'), { params: { limit: 500, format: 'json' }, headers: OCS_HEADERS })\n\t\tconst list = ocsData(res) || []\n\t\treturn Object.fromEntries((Array.isArray(list) ? list : []).map((entry) => [entry.userId, entry]))\n\t} catch {\n\t\treturn {}\n\t}\n}\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,WAAW,GAAG,EAAE,gBAAgB,EAAE,MAAM,GAAE;AAChD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,OAAO,CAAC,GAAG,EAAE;AACtB,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI;AACtE,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,eAAe,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,EAAE,EAAE;AACvD,CAAC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,sBAAsB,CAAC,EAAE;AACrE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE;AACvF,EAAE,OAAO,EAAE,WAAW;AACtB,EAAE,EAAC;AACH,CAAC,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,GAAE;AAC3D,CAAC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,MAAM;AAC/B,EAAE,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACtB,EAAE,IAAI,EAAE,KAAK,CAAC,WAAW,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AAC7C,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC;AACrC,EAAE,CAAC,CAAC;AACJ,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,eAAe,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE;AACxD,CAAC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,sCAAsC,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE;AAClG,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;AAC5B,EAAE,OAAO,EAAE,WAAW;AACtB,EAAE,EAAC;AACH,CAAC,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,KAAK,GAAE;AACzD,CAAC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;AAC3D,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,eAAe,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,EAAE,EAAE;AACtD,CAAC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,qBAAqB,CAAC,EAAE;AACpE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE;AACzD,EAAE,OAAO,EAAE,WAAW;AACtB,EAAE,EAAC;AACH,CAAC,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,KAAK,GAAE;AACzD,CAAC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;AAC3D,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,eAAe,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE;AAC3D,CAAC,IAAI;AACL,EAAE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,0CAA0C,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,EAAC;AAC3J,EAAE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAC;AAC1B,EAAE,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE;AAC1B,GAAG,MAAM,GAAG,GAAG,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAC;AACrG,GAAG,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,YAAY,IAAI,EAAE,EAAE;AAC1I,GAAG;AACH,EAAE,CAAC,MAAM;AACT;AACA,EAAE;AACF,CAAC,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AAC3C,EAAE,MAAM,OAAO,GAAG,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,GAAG,MAAM,CAAC,OAAO,GAAG,GAAE;AAC1E,EAAE,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,EAAE;AAClC,GAAG,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE;AACpD,GAAG;AACH,EAAE,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,KAAK,EAAE;AAC3D,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE;AACjD,GAAG;AACH,EAAE;AACF,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;AACtD,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,eAAe,aAAa,CAAC,KAAK,EAAE;AAC3C,CAAC,IAAI;AACL,EAAE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,kCAAkC,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,EAAC;AACnJ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,GAAE;AACjC,EAAE,OAAO,MAAM,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AACpG,EAAE,CAAC,MAAM;AACT,EAAE,OAAO,EAAE;AACX,EAAE;AACF;;;;"}
1
+ {"version":3,"file":"nextcloudDirectory.js","sources":["../../../src/utils/nextcloudDirectory.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n */\n\nimport { generateOcsUrl } from '@nextcloud/router'\n\n/**\n * Nextcloud's own directory: groups, the users in a group, and whether a\n * user is in today. Read through the OCS API with the reader's session, so\n * Nextcloud decides what the reader may see (listing a group's members needs\n * an admin or the group's sub-admin).\n *\n * @module utils/nextcloudDirectory\n */\n\nconst OCS_HEADERS = { 'OCS-APIRequest': 'true' }\n\n/**\n * The `ocs.data` of an OCS response, or null.\n *\n * @param {object} res An axios response.\n * @return {object|null}\n */\nfunction ocsData(res) {\n\treturn (res && res.data && res.data.ocs && res.data.ocs.data) || null\n}\n\n/** The most groups read, so a server that ignores the offset cannot loop forever. */\nconst MAX_GROUPS = 5000\n\n/**\n * The groups, with their member counts.\n *\n * @param {object} axios The axios instance.\n * @param {{search?: string, limit?: number}} [options] Narrowing; `limit` is the page size.\n * @return {Promise<Array<{id: string, name: string, count: number}>>}\n * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members\n */\nexport async function fetchGroups(axios, options = {}) {\n\tconst limit = options.limit || 200\n\t// Every page, while a page comes back full: one page cut an instance with\n\t// more groups than the limit off before its last ones (DqAfdelingen).\n\tconst groups = []\n\tfor (let offset = 0; offset < MAX_GROUPS; offset += limit) {\n\t\tconst res = await axios.get(generateOcsUrl('cloud/groups/details'), {\n\t\t\tparams: { search: options.search || '', limit, offset, format: 'json' },\n\t\t\theaders: OCS_HEADERS,\n\t\t})\n\t\tconst batch = (ocsData(res) && ocsData(res).groups) || []\n\t\tgroups.push(...batch)\n\t\tif (batch.length < limit) {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn groups.map((group) => ({\n\t\tid: String(group.id),\n\t\tname: group.displayname || String(group.id),\n\t\tcount: Number(group.usercount) || 0,\n\t}))\n}\n\n/**\n * The members of one group, with their details.\n *\n * @param {object} axios The axios instance.\n * @param {string} groupId The group id.\n * @return {Promise<Array<object>>} Nextcloud's user details, one per member.\n * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members\n */\nexport async function fetchGroupMembers(axios, groupId) {\n\tconst res = await axios.get(generateOcsUrl('cloud/groups/{groupId}/users/details', { groupId }), {\n\t\tparams: { format: 'json' },\n\t\theaders: OCS_HEADERS,\n\t})\n\tconst users = (ocsData(res) && ocsData(res).users) || {}\n\treturn Array.isArray(users) ? users : Object.values(users)\n}\n\n/**\n * Every user the reader may list, with their details: all users for an\n * admin, the users of their groups for a sub-admin. Refused for others.\n *\n * @param {object} axios The axios instance.\n * @param {{limit?: number}} [options] How many to read.\n * @return {Promise<Array<object>>} Nextcloud's user details, one per user.\n */\nexport async function fetchUsers(axios, options = {}) {\n\tconst res = await axios.get(generateOcsUrl('cloud/users/details'), {\n\t\tparams: { limit: options.limit || 500, format: 'json' },\n\t\theaders: OCS_HEADERS,\n\t})\n\tconst users = (ocsData(res) && ocsData(res).users) || {}\n\treturn Array.isArray(users) ? users : Object.values(users)\n}\n\n/**\n * Whether a user is in today: an out-of-office period that runs now (the\n * dav app's absence, Nextcloud 28 and later), else their user status.\n *\n * @param {object} axios The axios instance.\n * @param {string} userId The user id.\n * @param {object|null} status The user's entry from the user status list, or null.\n * @return {Promise<{state: string, until: (string|null), message: string}>}\n * `state` is `absent` (with `until`, an ISO date), `away`, `present` or `unknown`.\n * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members\n */\nexport async function fetchPresence(axios, userId, status) {\n\ttry {\n\t\tconst res = await axios.get(generateOcsUrl('apps/dav/api/v1/outOfOffice/{userId}/now', { userId }), { params: { format: 'json' }, headers: OCS_HEADERS })\n\t\tconst ooo = ocsData(res)\n\t\tif (ooo && ooo.endDate) {\n\t\t\tconst end = typeof ooo.endDate === 'number' ? new Date(ooo.endDate * 1000) : new Date(ooo.endDate)\n\t\t\treturn { state: 'absent', until: Number.isNaN(end.getTime()) ? null : end.toISOString().slice(0, 10), message: ooo.shortMessage || '' }\n\t\t}\n\t} catch {\n\t\t// 404: no absence now, or an instance without the absence API.\n\t}\n\tif (status && typeof status === 'object') {\n\t\tconst message = typeof status.message === 'string' ? status.message : ''\n\t\tif (status.status === 'online') {\n\t\t\treturn { state: 'present', until: null, message }\n\t\t}\n\t\tif (status.status === 'away' || status.status === 'dnd') {\n\t\t\treturn { state: 'away', until: null, message }\n\t\t}\n\t}\n\treturn { state: 'unknown', until: null, message: '' }\n}\n\n/**\n * The user statuses Nextcloud shares with the reader, by user id. Empty when\n * the user status app is off.\n *\n * @param {object} axios The axios instance.\n * @return {Promise<Record<string, object>>}\n * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members\n */\nexport async function fetchStatuses(axios) {\n\ttry {\n\t\tconst res = await axios.get(generateOcsUrl('apps/user_status/api/v1/statuses'), { params: { limit: 500, format: 'json' }, headers: OCS_HEADERS })\n\t\tconst list = ocsData(res) || []\n\t\treturn Object.fromEntries((Array.isArray(list) ? list : []).map((entry) => [entry.userId, entry]))\n\t} catch {\n\t\treturn {}\n\t}\n}\n"],"names":[],"mappings":"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"}