@conduction/nextcloud-vue 2.75.0 → 2.77.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 (158) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +18 -3
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +49 -3
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +6 -0
  6. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  7. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -2
  8. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  9. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +42 -1
  10. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +228 -203
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  13. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
  14. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
  15. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
  16. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
  17. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
  18. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
  19. package/dist/esm/components/CnCredentials/CnCredentials.vue2.js +11 -10
  20. package/dist/esm/components/CnCredentials/CnCredentials.vue2.js.map +1 -1
  21. package/dist/esm/components/CnCredentials/CnCredentials.vue3.js.map +1 -1
  22. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +41 -4
  23. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +24 -33
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  30. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
  31. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
  34. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  35. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
  36. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
  38. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
  40. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +242 -19
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +25 -20
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/selfModeActions.js +90 -14
  46. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  47. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +15 -3
  48. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  49. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +26 -0
  50. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  51. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +26 -9
  52. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  53. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
  54. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  55. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
  56. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +19 -3
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +6 -0
  61. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  64. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +153 -38
  65. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +74 -56
  67. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  68. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  69. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  70. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  71. package/dist/esm/composables/useListView.js +4 -0
  72. package/dist/esm/composables/useListView.js.map +1 -1
  73. package/dist/esm/composables/useObjectLock.js +41 -10
  74. package/dist/esm/composables/useObjectLock.js.map +1 -1
  75. package/dist/esm/index.js +3 -1
  76. package/dist/esm/index.js.map +1 -1
  77. package/dist/esm/l10n/en.json.js +31 -1
  78. package/dist/esm/l10n/en.json.js.map +1 -1
  79. package/dist/esm/l10n/nl.json.js +32 -2
  80. package/dist/esm/l10n/nl.json.js.map +1 -1
  81. package/dist/esm/nextcloud-vue.css +376 -75
  82. package/dist/esm/store/plugins/interactions.js +20 -10
  83. package/dist/esm/store/plugins/interactions.js.map +1 -1
  84. package/dist/esm/store/useObjectStore.js +54 -9
  85. package/dist/esm/store/useObjectStore.js.map +1 -1
  86. package/dist/esm/utils/builtInFormatters.js +15 -7
  87. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  88. package/dist/esm/utils/groupAutocomplete.js +63 -1
  89. package/dist/esm/utils/groupAutocomplete.js.map +1 -1
  90. package/dist/esm/utils/lensAvailability.js +90 -0
  91. package/dist/esm/utils/lensAvailability.js.map +1 -0
  92. package/dist/esm/utils/objectSignals.js +44 -0
  93. package/dist/esm/utils/objectSignals.js.map +1 -0
  94. package/dist/esm/utils/personalLenses.js +13 -6
  95. package/dist/esm/utils/personalLenses.js.map +1 -1
  96. package/dist/esm/utils/recordInteractions.js +31 -13
  97. package/dist/esm/utils/recordInteractions.js.map +1 -1
  98. package/dist/esm/utils/resolveFilterTokens.js +46 -3
  99. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  100. package/dist/esm/utils/schema.js +1 -1
  101. package/dist/esm/utils/schema.js.map +1 -1
  102. package/dist/esm/utils/sentinelTokens.js +4 -4
  103. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  104. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  105. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  106. package/dist/esm/utils/widgetVisibility.js +46 -1
  107. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  108. package/dist/nextcloud-vue.cjs.js +4732 -2990
  109. package/dist/nextcloud-vue.cjs.js.map +2 -2
  110. package/dist/nextcloud-vue.css +376 -75
  111. package/l10n/en.json +31 -1
  112. package/l10n/nl.json +32 -2
  113. package/package.json +1 -1
  114. package/src/components/CnAppNav/CnAppNav.vue +44 -5
  115. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  116. package/src/components/CnCardGrid/CnCardGrid.vue +7 -0
  117. package/src/components/CnCellRenderer/CnCellRenderer.vue +56 -1
  118. package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
  119. package/src/components/CnCredentials/CnCredentials.vue +17 -16
  120. package/src/components/CnDataTable/CnDataTable.md +9 -0
  121. package/src/components/CnDataTable/CnDataTable.vue +80 -20
  122. package/src/components/CnDetailPage/CnDetailPage.vue +18 -32
  123. package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
  124. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
  125. package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
  126. package/src/components/CnIndexPage/CnIndexPage.md +51 -0
  127. package/src/components/CnIndexPage/CnIndexPage.vue +261 -35
  128. package/src/components/CnIndexPage/selfModeActions.js +89 -13
  129. package/src/components/CnIndexPage/useSelfFetchList.js +15 -3
  130. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  131. package/src/components/CnObjectCard/CnObjectCard.vue +89 -1
  132. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
  133. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +19 -3
  134. package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
  135. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +181 -38
  136. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  137. package/src/composables/__tests__/useObjectLock.endpoints.spec.js +83 -0
  138. package/src/composables/useListView.js +4 -0
  139. package/src/composables/useObjectLock.js +41 -10
  140. package/src/css/look-board-detail.css +40 -2
  141. package/src/css/look-board-index.css +35 -1
  142. package/src/css/table.css +82 -17
  143. package/src/index.js +3 -1
  144. package/src/schemas/app-manifest-v2.schema.json +11 -7
  145. package/src/store/plugins/interactions.js +20 -10
  146. package/src/store/useObjectStore.js +54 -9
  147. package/src/utils/builtInFormatters.js +15 -7
  148. package/src/utils/groupAutocomplete.js +75 -0
  149. package/src/utils/index.js +2 -1
  150. package/src/utils/lensAvailability.js +86 -0
  151. package/src/utils/objectSignals.js +41 -0
  152. package/src/utils/personalLenses.js +13 -6
  153. package/src/utils/recordInteractions.js +31 -13
  154. package/src/utils/resolveFilterTokens.js +45 -2
  155. package/src/utils/schema.js +1 -1
  156. package/src/utils/sentinelTokens.js +4 -4
  157. package/src/utils/validateManifestV2.compiled.js +2 -2
  158. package/src/utils/widgetVisibility.js +45 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * Signal that objects changed outside the surface showing them, such as files
6
+ * uploaded or an object saved from an app's own modal. CnIndexPage listens and
7
+ * refreshes its list when the register and schema match.
8
+ *
9
+ * @module utils/objectSignals
10
+ */
11
+
12
+ export const OBJECTS_CHANGED_EVENT = 'cn:objects-changed'
13
+
14
+ /**
15
+ * Announce that objects of a register and schema changed.
16
+ *
17
+ * Pass the same register and schema identifiers (slug or id) the listing page
18
+ * uses. Leaving one out matches every page on that part.
19
+ *
20
+ * @param {object} [payload] What changed.
21
+ * @param {string|number} [payload.register] The register slug or id.
22
+ * @param {string|number} [payload.schema] The schema slug or id.
23
+ * @param {string} [payload.id] The changed object's id, when there is one.
24
+ * @return {boolean} True when the event was dispatched.
25
+ */
26
+ export function dispatchObjectsChanged({ register, schema, id } = {}) {
27
+ if (typeof window === 'undefined') {
28
+ return false
29
+ }
30
+ const detail = {
31
+ register: register === undefined || register === null || register === '' ? null : String(register),
32
+ schema: schema === undefined || schema === null || schema === '' ? null : String(schema),
33
+ id: id ? String(id) : null,
34
+ }
35
+ try {
36
+ window.dispatchEvent(new CustomEvent(OBJECTS_CHANGED_EVENT, { detail }))
37
+ return true
38
+ } catch {
39
+ return false
40
+ }
41
+ }
@@ -2,30 +2,37 @@
2
2
  * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
3
  * SPDX-License-Identifier: EUPL-1.2
4
4
  *
5
- * The personal lenses of an index page (Favourites, Recent, Following) as
6
- * quick filters, appended after the page's own.
5
+ * The personal lenses of an index page (Following, Recent, Unread) as quick
6
+ * filters, appended after the page's own.
7
7
  *
8
- * @spec openspec/changes/record-favourite-and-follow/tasks.md#task-3
8
+ * There is no Favourites lens any more: a favourite is a follow with
9
+ * notifications off (OpenRegister `merge-follow-and-favourites`), so
10
+ * `favourite` is an alias of `watching` and asking for both gives one tab.
11
+ *
12
+ * @spec openspec/changes/one-follow-control/specs/record-follow/spec.md#requirement-index-pages-offer-one-following-lens-and-a-follow-column
9
13
  */
10
14
  import { translate as t } from '@nextcloud/l10n'
11
15
 
12
16
  /** Lens id to the query key OpenRegister reads. */
13
- export const LENS_FILTER_KEYS = { favourite: '_favourite', recent: '_recent', watching: '_watching', unread: '_unread' }
17
+ export const LENS_FILTER_KEYS = { recent: '_recent', watching: '_watching', unread: '_unread' }
18
+
19
+ /** Deprecated lens ids and the lens they now mean. */
20
+ const LENS_ALIASES = { favourite: 'watching' }
14
21
 
15
22
  /**
16
23
  * Quick filters for the requested lenses. Unknown ids are ignored.
17
24
  *
18
- * @param {string[]} lenses Any of `favourite`, `recent`, `watching`, `unread`.
25
+ * @param {string[]} lenses Any of `watching`, `recent`, `unread`, and the deprecated `favourite` (read as `watching`).
19
26
  * @return {Array<{label: string, filter: object, lens: string}>} One tab per lens, in the order asked.
20
27
  */
21
28
  export function personalLensTabs(lenses) {
22
29
  const labels = {
23
- favourite: t('nextcloud-vue', 'Favourites'),
24
30
  recent: t('nextcloud-vue', 'Recent'),
25
31
  watching: t('nextcloud-vue', 'Following'),
26
32
  unread: t('nextcloud-vue', 'Unread'),
27
33
  }
28
34
  return (Array.isArray(lenses) ? lenses : [])
35
+ .map((id) => (Object.hasOwn(LENS_ALIASES, id) ? LENS_ALIASES[id] : id))
29
36
  .filter((id, i, all) => Object.hasOwn(LENS_FILTER_KEYS, id) && all.indexOf(id) === i)
30
37
  .map((id) => ({ label: labels[id], filter: { [LENS_FILTER_KEYS[id]]: true }, lens: id }))
31
38
  }
@@ -2,8 +2,10 @@
2
2
  * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
3
  * SPDX-License-Identifier: EUPL-1.2
4
4
  *
5
- * The per-user interaction calls OpenRegister serves on a record: favourite,
6
- * watch (follow) and the watchers list. Plain functions over `fetch`, shared
5
+ * The per-user interaction calls OpenRegister serves on a record: follow
6
+ * (watch, with its notifications switch), the watchers list, the read state,
7
+ * and the deprecated favourite (a quiet follow since OpenRegister's
8
+ * `merge-follow-and-favourites`). Plain functions over `fetch`, shared
7
9
  * by the interactions store plugin and by the toggles, so a toggle works with
8
10
  * or without the plugin installed.
9
11
  *
@@ -31,13 +33,18 @@ export function interactionUrl(register, schema, id, tail) {
31
33
  /**
32
34
  * Send one interaction call and normalise the answer. Never throws.
33
35
  *
34
- * @param {string} method HTTP method.
35
- * @param {string} url Full URL.
36
+ * @param {string} method HTTP method.
37
+ * @param {string} url Full URL.
38
+ * @param {object|null} [body] JSON body, or null for none.
36
39
  * @return {Promise<{ok: boolean, status: number|null, data: (object|null), message: string}>} The outcome; `message` is the server's, or a network message.
37
40
  */
38
- async function call(method, url) {
41
+ async function call(method, url, body = null) {
39
42
  try {
40
- const response = await fetch(prefixUrl(url), { method, headers: buildHeaders() })
43
+ const init = { method, headers: buildHeaders() }
44
+ if (body !== null) {
45
+ init.body = JSON.stringify(body)
46
+ }
47
+ const response = await fetch(prefixUrl(url), init)
41
48
  if (!response.ok) {
42
49
  const error = await parseResponseError(response, 'record')
43
50
  // The server's own words win over the generic status text.
@@ -61,6 +68,9 @@ async function call(method, url) {
61
68
  /**
62
69
  * Star or unstar a record for the current user.
63
70
  *
71
+ * @deprecated A star is a quiet follow since OpenRegister's
72
+ * `merge-follow-and-favourites`; use `setWatching(..., { notify: false })`.
73
+ *
64
74
  * @param {string} register Register slug.
65
75
  * @param {string} schema Schema slug.
66
76
  * @param {string} id Object id.
@@ -72,16 +82,24 @@ export function setFavourite(register, schema, id, on) {
72
82
  }
73
83
 
74
84
  /**
75
- * Follow or unfollow a record for the current user.
85
+ * Follow or unfollow a record for the current user, optionally setting the
86
+ * follow's notifications switch.
76
87
  *
77
- * @param {string} register Register slug.
78
- * @param {string} schema Schema slug.
79
- * @param {string} id Object id.
80
- * @param {boolean} on True to follow, false to unfollow.
88
+ * `options.notify` is sent only when following and only when it is a boolean:
89
+ * left out, a new follow notifies and an existing follow keeps its setting.
90
+ *
91
+ * @param {string} register Register slug.
92
+ * @param {string} schema Schema slug.
93
+ * @param {string} id Object id.
94
+ * @param {boolean} on True to follow, false to unfollow.
95
+ * @param {object} [options] Options.
96
+ * @param {boolean} [options.notify] The notifications switch.
81
97
  * @return {Promise<object>} The outcome (see `call`).
98
+ * @spec openspec/changes/one-follow-control/specs/record-follow/spec.md#requirement-one-follow-control-with-a-notifications-switch
82
99
  */
83
- export function setWatching(register, schema, id, on) {
84
- return call(on ? 'PUT' : 'DELETE', interactionUrl(register, schema, id, 'watch'))
100
+ export function setWatching(register, schema, id, on, options = {}) {
101
+ const body = on && options && typeof options.notify === 'boolean' ? { notify: options.notify } : null
102
+ return call(on ? 'PUT' : 'DELETE', interactionUrl(register, schema, id, 'watch'), body)
85
103
  }
86
104
 
87
105
  /**
@@ -10,6 +10,13 @@
10
10
  * Supported tokens:
11
11
  * - `@me` → the current Nextcloud user id (`getCurrentUser()` from
12
12
  * `@nextcloud/auth`; `window.OC.currentUser` fallback)
13
+ * - `@myGroups` → the ids of the Nextcloud groups the current user is
14
+ * in, as an ARRAY (an IN filter). `ctx.myGroups` wins;
15
+ * otherwise a reactive peek over `getCurrentUserGroups`.
16
+ * Stays UNRESOLVED while loading and when the user is
17
+ * in no group, so the caller waits instead of sending
18
+ * an empty IN list (which OpenRegister reads as "no
19
+ * constraint"). Spread in place inside an IN-list array.
13
20
  * - `@now` → the current instant, ISO-8601
14
21
  * - `@today` → today at 00:00, `YYYY-MM-DD` (date-prefix comparable)
15
22
  * - `@today±Nd` → relative-date arithmetic at day granularity: N days
@@ -56,6 +63,34 @@
56
63
 
57
64
  import { getCurrentUser } from '@nextcloud/auth'
58
65
  import { TODAY_DELTA_RE } from './sentinelTokens.js'
66
+ import { peekCurrentUserGroups } from './widgetVisibility.js'
67
+
68
+ /** The filter token for the current user's group ids. */
69
+ export const MY_GROUPS_TOKEN = '@myGroups'
70
+
71
+ /**
72
+ * Resolve `@myGroups`: the current user's group ids, or the token itself while
73
+ * they are loading or when there are none (an empty IN list would match every
74
+ * row, so "no team" must mean "wait", never "everything").
75
+ *
76
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/schema-utilities/spec.md#requirement-mygroups-resolves-to-the-current-users-group-ids
77
+ * @param {{myGroups?: string[]}} [ctx] Optional context; `ctx.myGroups` wins.
78
+ * @return {string[]|string} The group ids, or `'@myGroups'` when unresolved.
79
+ */
80
+ function resolveMyGroups(ctx) {
81
+ let groups = ctx && Array.isArray(ctx.myGroups) ? ctx.myGroups : null
82
+ if (groups === null) {
83
+ try {
84
+ groups = peekCurrentUserGroups()
85
+ } catch {
86
+ groups = null
87
+ }
88
+ }
89
+ const ids = Array.isArray(groups)
90
+ ? groups.filter((g) => typeof g === 'string' && g !== '')
91
+ : []
92
+ return ids.length > 0 ? [...ids] : MY_GROUPS_TOKEN
93
+ }
59
94
 
60
95
  /**
61
96
  * Format a Date as `YYYY-MM-DD` (local).
@@ -73,7 +108,7 @@ function ymd(d) {
73
108
  * Resolve a single filter value if it is a dynamic `@`-token, else pass through.
74
109
  *
75
110
  * @param {unknown} v The candidate value.
76
- * @param {{objectId?: (string|number), object?: object, workspace?: object, config?: object, range?: object}} [ctx] Optional
111
+ * @param {{objectId?: (string|number), object?: object, workspace?: object, config?: object, range?: object, myGroups?: string[]}} [ctx] Optional
77
112
  * context for `@objectId` / `@object.<field>` (detail page),
78
113
  * `@workspace.<key>` (page-level workspace state), and `@config.<key>`
79
114
  * (page-level app config) tokens.
@@ -84,6 +119,9 @@ export function resolveFilterValue(v, ctx) {
84
119
  return v
85
120
  }
86
121
  const now = new Date()
122
+ if (v === MY_GROUPS_TOKEN) {
123
+ return resolveMyGroups(ctx)
124
+ }
87
125
  if (v === '@objectId') {
88
126
  return (ctx && ctx.objectId !== undefined && ctx.objectId !== null) ? String(ctx.objectId) : v
89
127
  }
@@ -313,8 +351,13 @@ export function resolveFilterTokens(filter, ctx) {
313
351
  if (!filter || typeof filter !== 'object') {
314
352
  return filter
315
353
  }
354
+ // An item that resolves to a list (`@myGroups`) is spread in place, so
355
+ // `['@myGroups', 'archive']` stays one flat IN list.
316
356
  const resolveOne = (v) => (Array.isArray(v)
317
- ? v.map((item) => resolveFilterValue(item, ctx))
357
+ ? v.flatMap((item) => {
358
+ const r = resolveFilterValue(item, ctx)
359
+ return Array.isArray(r) ? r : [r]
360
+ })
318
361
  : resolveFilterValue(v, ctx))
319
362
  const out = {}
320
363
  for (const [k, v] of Object.entries(filter)) {
@@ -385,7 +385,7 @@ function isUserProp(prop) {
385
385
  * @param {object} prop A schema property definition (or `items` for an array).
386
386
  * @return {boolean} True when the property marks a Nextcloud group.
387
387
  */
388
- function isGroupProp(prop) {
388
+ export function isGroupProp(prop) {
389
389
  if (!prop || typeof prop !== 'object') {
390
390
  return false
391
391
  }
@@ -26,7 +26,7 @@
26
26
  * A token is valid only in the context whose resolver runs on it:
27
27
  *
28
28
  * - **filter** — `resolveFilterTokens`, resolved at fetch time:
29
- * `@me`, `@now`, `@today`, `@today±Nd`, `@monthStart`,
29
+ * `@me`, `@myGroups`, `@now`, `@today`, `@today±Nd`, `@monthStart`,
30
30
  * `@quarterStart`, `@yearStart`.
31
31
  * - **config** — `IAppConfig`-sourced: `@resolve:<key>`, `@config.<key>`
32
32
  * (trailing `?` marks optional).
@@ -67,7 +67,7 @@
67
67
  * @type {Readonly<Record<string, string>>}
68
68
  */
69
69
  export const SENTINEL_TOKEN_PATTERNS = Object.freeze({
70
- filter: '^@(?:me(?:\\.(?:displayName|email))?|now|today|monthStart|quarterStart|yearStart)$|^@today[+-][0-9]+d$',
70
+ filter: '^@(?:me(?:\\.(?:displayName|email))?|myGroups|now|today|monthStart|quarterStart|yearStart)$|^@today[+-][0-9]+d$',
71
71
  config: '^@resolve:[a-z][a-z0-9_-]*$|^@config\\.[A-Za-z][A-Za-z0-9_.]*\\??$',
72
72
  object: '^@objectId$|^@object\\.[A-Za-z][A-Za-z0-9_]*$',
73
73
  workspace: '^@workspace\\.[A-Za-z][A-Za-z0-9_]*\\??$',
@@ -116,8 +116,8 @@ export const SENTINEL_CONTEXTS = Object.freeze([
116
116
  export const SENTINEL_VOCABULARY = Object.freeze({
117
117
  filter: {
118
118
  resolver: 'resolveFilterTokens',
119
- description: 'Relative fetch-time tokens (current user / relative dates).',
120
- members: ['@me', '@me.displayName', '@me.email', '@now', '@today', '@today±Nd', '@monthStart', '@quarterStart', '@yearStart'],
119
+ description: 'Relative fetch-time tokens (current user, the current user\'s groups as an IN list, relative dates).',
120
+ members: ['@me', '@me.displayName', '@me.email', '@myGroups', '@now', '@today', '@today±Nd', '@monthStart', '@quarterStart', '@yearStart'],
121
121
  },
122
122
  config: {
123
123
  resolver: 'resolveManifestSentinels / resolveFilterTokens',