@conduction/nextcloud-vue 2.106.0 → 2.107.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 (135) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +9 -0
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js.map +1 -0
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +338 -0
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -0
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +249 -0
  6. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAdvancedFormDialog/CnPropertiesTab.vue2.js +2 -2
  8. package/dist/esm/components/CnAdvancedFormDialog/CnPropertiesTab.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  10. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +145 -138
  11. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +102 -2
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  14. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +83 -6
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +4 -2
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  21. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  22. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +389 -20
  23. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  24. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +68 -35
  25. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  26. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +22 -2
  27. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  28. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +69 -5
  29. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  30. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +38 -5
  31. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  32. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  33. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +7 -1
  34. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  35. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  36. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +8 -0
  37. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -0
  38. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +297 -0
  39. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -0
  40. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +24 -0
  41. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -0
  42. package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue.js +9 -0
  43. package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue.js.map +1 -0
  44. package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue2.js +95 -0
  45. package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue2.js.map +1 -0
  46. package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue3.js +123 -0
  47. package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue3.js.map +1 -0
  48. package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue.js +1 -1
  49. package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue2.js +13 -5
  50. package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue2.js.map +1 -1
  51. package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue3.js +3 -1
  52. package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue3.js.map +1 -1
  53. package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue.js +1 -1
  54. package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue2.js +49 -23
  55. package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue2.js.map +1 -1
  56. package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue3.js +74 -27
  57. package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue3.js.map +1 -1
  58. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue.js +0 -1
  59. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue.js.map +1 -1
  60. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js +18 -413
  61. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  62. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +27 -214
  63. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  64. package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue.js +9 -0
  65. package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue.js.map +1 -0
  66. package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue2.js +190 -0
  67. package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue2.js.map +1 -0
  68. package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue3.js +270 -0
  69. package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue3.js.map +1 -0
  70. package/dist/esm/index.js +4 -0
  71. package/dist/esm/index.js.map +1 -1
  72. package/dist/esm/l10n/en.json.js +56 -1
  73. package/dist/esm/l10n/en.json.js.map +1 -1
  74. package/dist/esm/l10n/nl.json.js +56 -1
  75. package/dist/esm/l10n/nl.json.js.map +1 -1
  76. package/dist/esm/library-style.js +1 -1
  77. package/dist/esm/mixins/savedViewsList.js +235 -0
  78. package/dist/esm/mixins/savedViewsList.js.map +1 -0
  79. package/dist/esm/nextcloud-vue.css +348 -108
  80. package/dist/esm/utils/actionsDispatcher.js +2 -2
  81. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  82. package/dist/esm/utils/builtInModals.js +39 -0
  83. package/dist/esm/utils/builtInModals.js.map +1 -0
  84. package/dist/esm/utils/mapMarkerColor.js +121 -0
  85. package/dist/esm/utils/mapMarkerColor.js.map +1 -0
  86. package/dist/esm/utils/mergeManifestDelta.js +4 -0
  87. package/dist/esm/utils/mergeManifestDelta.js.map +1 -1
  88. package/dist/esm/utils/savedViewHelpers.js +59 -7
  89. package/dist/esm/utils/savedViewHelpers.js.map +1 -1
  90. package/dist/esm/utils/savedViewMenu.js +154 -0
  91. package/dist/esm/utils/savedViewMenu.js.map +1 -0
  92. package/dist/esm/utils/searchGroupSharees.js +50 -15
  93. package/dist/esm/utils/searchGroupSharees.js.map +1 -1
  94. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  95. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  96. package/dist/nextcloud-vue.cjs.js +13351 -10855
  97. package/dist/nextcloud-vue.cjs.js.map +2 -2
  98. package/dist/nextcloud-vue.css +348 -108
  99. package/l10n/en.json +56 -1
  100. package/l10n/nl.json +56 -1
  101. package/package.json +1 -1
  102. package/src/components/CnActionDialog/CnActionDialog.vue +545 -0
  103. package/src/components/CnActionDialog/index.js +12 -0
  104. package/src/components/CnAppNav/CnAppNav.vue +17 -0
  105. package/src/components/CnAppRoot/CnAppRoot.vue +102 -2
  106. package/src/components/CnDashboardPage/CnDashboardPage.vue +86 -7
  107. package/src/components/CnIndexPage/CnIndexPage.vue +409 -23
  108. package/src/components/CnIndexPage/useSelfFetchList.js +21 -1
  109. package/src/components/CnIndexSidebar/CnIndexSidebar.md +2 -0
  110. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +91 -1
  111. package/src/components/CnMapWidget/CnMapWidget.vue +16 -1
  112. package/src/components/CnPartyPicker/CnPartyPicker.vue +317 -0
  113. package/src/components/CnPartyPicker/index.js +11 -0
  114. package/src/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue +192 -0
  115. package/src/components/CnSavedViewEditDialog/index.js +11 -0
  116. package/src/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue +21 -6
  117. package/src/components/CnSavedViewShareFields/CnSavedViewShareFields.vue +130 -37
  118. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +36 -599
  119. package/src/components/CnSavedViewsPanel/CnSavedViewsPanel.vue +459 -0
  120. package/src/components/CnSavedViewsPanel/index.js +11 -0
  121. package/src/components/index.js +4 -0
  122. package/src/css/index-page.css +16 -0
  123. package/src/css/look-board-index.css +5 -3
  124. package/src/css/look-board.css +18 -3
  125. package/src/index.js +4 -0
  126. package/src/mixins/savedViewsList.js +231 -0
  127. package/src/schemas/app-manifest-v2.schema.json +98 -3
  128. package/src/utils/actionsDispatcher.js +2 -2
  129. package/src/utils/builtInModals.js +35 -0
  130. package/src/utils/mapMarkerColor.js +117 -0
  131. package/src/utils/mergeManifestDelta.js +4 -0
  132. package/src/utils/savedViewHelpers.js +58 -6
  133. package/src/utils/savedViewMenu.js +150 -0
  134. package/src/utils/searchGroupSharees.js +49 -14
  135. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -0,0 +1,192 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <NcDialog
7
+ :name="resolvedTitle"
8
+ size="small"
9
+ :noClose="loading"
10
+ @closing="onClose">
11
+ <div class="cn-edit-view" data-testid="cn-modal" data-testid-modal="cn-saved-view-edit-dialog">
12
+ <NcNoteCard v-if="error" type="error">
13
+ {{ error }}
14
+ </NcNoteCard>
15
+ <p class="cn-edit-view__intro">
16
+ {{ t('nextcloud-vue', 'Give the view another name, or replace its criteria with what the list shows now.') }}
17
+ </p>
18
+ <NcTextField
19
+ :modelValue="name"
20
+ :label="t('nextcloud-vue', 'Name')"
21
+ :labelVisible="true"
22
+ data-testid="cn-saved-view-edit-name"
23
+ @update:modelValue="(v) => name = v" />
24
+ <fieldset class="cn-edit-view__criteria" data-testid="cn-saved-view-edit-replace">
25
+ <legend>{{ t('nextcloud-vue', 'Criteria') }}</legend>
26
+ <NcCheckboxRadioSwitch
27
+ type="radio"
28
+ name="cn-edit-view-criteria"
29
+ value="keep"
30
+ :modelValue="replaceCriteria ? 'replace' : 'keep'"
31
+ data-testid="cn-saved-view-edit-keep"
32
+ @update:modelValue="replaceCriteria = false">
33
+ {{ t('nextcloud-vue', 'Keep what is saved') }}
34
+ </NcCheckboxRadioSwitch>
35
+ <NcCheckboxRadioSwitch
36
+ type="radio"
37
+ name="cn-edit-view-criteria"
38
+ value="replace"
39
+ :modelValue="replaceCriteria ? 'replace' : 'keep'"
40
+ data-testid="cn-saved-view-edit-replace-current"
41
+ @update:modelValue="replaceCriteria = true">
42
+ {{ t('nextcloud-vue', 'Replace with the current search term, filters and sort') }}
43
+ </NcCheckboxRadioSwitch>
44
+ </fieldset>
45
+ <p v-if="isShared" class="cn-edit-view__note">
46
+ {{ t('nextcloud-vue', 'Whoever you share the view with sees the change at once.') }}
47
+ </p>
48
+ </div>
49
+ <template #actions>
50
+ <NcButton :disabled="loading" @click="onClose">
51
+ {{ t('nextcloud-vue', 'Cancel') }}
52
+ </NcButton>
53
+ <NcButton
54
+ variant="primary"
55
+ :disabled="loading || !name.trim()"
56
+ data-testid="cn-saved-view-edit-confirm"
57
+ @click="onConfirm">
58
+ <template #icon>
59
+ <NcLoadingIcon v-if="loading" :size="20" :name="t('nextcloud-vue', 'Loading …')" />
60
+ </template>
61
+ {{ t('nextcloud-vue', 'Save') }}
62
+ </NcButton>
63
+ </template>
64
+ </NcDialog>
65
+ </template>
66
+
67
+ <script>
68
+ import { translate as t } from '@nextcloud/l10n'
69
+ import { NcButton, NcCheckboxRadioSwitch, NcDialog, NcLoadingIcon, NcNoteCard, NcTextField } from '@nextcloud/vue'
70
+
71
+ /**
72
+ * CnSavedViewEditDialog — rename an own saved view and, when asked, replace
73
+ * its criteria with the filters, search and sort the list shows now. Opened by
74
+ * `CnIndexPage` from the Edit entry of `CnSavedViewsControl`. Emits
75
+ * `confirm({ name, replaceCriteria })`; the parent saves and either closes the
76
+ * dialog or reports a failure through `setError()`, which keeps the form open.
77
+ *
78
+ * @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-3
79
+ */
80
+ export default {
81
+ name: 'CnSavedViewEditDialog',
82
+
83
+ components: { NcButton, NcCheckboxRadioSwitch, NcDialog, NcLoadingIcon, NcNoteCard, NcTextField },
84
+
85
+ props: {
86
+ /** The view being edited (its `name` fills the field). */
87
+ view: {
88
+ type: Object,
89
+ default: () => ({}),
90
+ },
91
+
92
+ /** Dialog title; empty reads "Edit view: {name}". */
93
+ dialogTitle: {
94
+ type: String,
95
+ default: '',
96
+ },
97
+ },
98
+
99
+ emits: ['close', 'confirm'],
100
+
101
+ data() {
102
+ return {
103
+ name: (this.view && typeof this.view.name === 'string') ? this.view.name : '',
104
+ replaceCriteria: false,
105
+ loading: false,
106
+ error: '',
107
+ }
108
+ },
109
+
110
+ computed: {
111
+ /** @return {string} The title. */
112
+ resolvedTitle() {
113
+ return this.dialogTitle || t('nextcloud-vue', 'Edit view: {name}', { name: (this.view && this.view.name) || '' })
114
+ },
115
+
116
+ /** @return {boolean} Whether the view is shared with anyone. */
117
+ isShared() {
118
+ return Array.isArray(this.view && this.view.sharedWith) && this.view.sharedWith.length > 0
119
+ },
120
+ },
121
+
122
+ methods: {
123
+ t,
124
+
125
+ /**
126
+ * Save clicked with a non-empty name.
127
+ *
128
+ * @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-3
129
+ */
130
+ onConfirm() {
131
+ if (!this.name.trim()) {
132
+ return
133
+ }
134
+ this.loading = true
135
+ this.error = ''
136
+ /**
137
+ * @event confirm Save clicked. Payload: `{ name, replaceCriteria }`.
138
+ * @type {object}
139
+ */
140
+ this.$emit('confirm', { name: this.name.trim(), replaceCriteria: this.replaceCriteria })
141
+ },
142
+
143
+ onClose() {
144
+ /** @event close Dialog dismissed. */
145
+ this.$emit('close')
146
+ },
147
+
148
+ /**
149
+ * Parent-callable (via ref): show a refused save and re-enable the form.
150
+ *
151
+ * @param {string} message The server's message.
152
+ * @public
153
+ */
154
+ setError(message) {
155
+ this.loading = false
156
+ this.error = message || t('nextcloud-vue', 'Could not save the view')
157
+ },
158
+ },
159
+ }
160
+ </script>
161
+
162
+ <style scoped>
163
+ .cn-edit-view__intro,
164
+ .cn-edit-view__note {
165
+ margin: 0;
166
+ }
167
+
168
+ .cn-edit-view__note {
169
+ color: var(--color-text-maxcontrast);
170
+ }
171
+
172
+ .cn-edit-view__criteria {
173
+ display: flex;
174
+ flex-direction: column;
175
+ gap: 4px;
176
+ border: 0;
177
+ margin: 0;
178
+ padding: 0;
179
+ }
180
+
181
+ .cn-edit-view__criteria legend {
182
+ font-weight: bold;
183
+ margin-bottom: 4px;
184
+ }
185
+
186
+ .cn-edit-view {
187
+ display: flex;
188
+ flex-direction: column;
189
+ gap: 12px;
190
+ padding-block: 8px;
191
+ }
192
+ </style>
@@ -0,0 +1,11 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnSavedViewEditDialog — rename a saved view, optionally with the current filters.
6
+ */
7
+
8
+ import CnSavedViewEditDialog from './CnSavedViewEditDialog.vue'
9
+
10
+ export { CnSavedViewEditDialog }
11
+ export default CnSavedViewEditDialog
@@ -4,7 +4,7 @@
4
4
  -->
5
5
  <template>
6
6
  <NcDialog
7
- :name="dialogTitle"
7
+ :name="resolvedTitle"
8
8
  size="small"
9
9
  :noClose="loading"
10
10
  @closing="onClose">
@@ -12,6 +12,9 @@
12
12
  <NcNoteCard v-if="error" type="error">
13
13
  {{ error }}
14
14
  </NcNoteCard>
15
+ <p class="cn-share-view__intro">
16
+ {{ t('nextcloud-vue', 'Whoever gets the view sees it under Shared with me. It is yours and stays yours.') }}
17
+ </p>
15
18
  <CnSavedViewShareFields v-model="sharedWith" />
16
19
  </div>
17
20
  <template #actions>
@@ -36,6 +39,7 @@
36
39
  import { translate as t } from '@nextcloud/l10n'
37
40
  import { NcButton, NcDialog, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
38
41
  import CnSavedViewShareFields from '../CnSavedViewShareFields/CnSavedViewShareFields.vue'
42
+ import { normalizeSharedWith } from '../../utils/savedViewHelpers.js'
39
43
 
40
44
  /**
41
45
  * CnSavedViewShareDialog — change who a saved view is shared with. Opened by
@@ -55,10 +59,10 @@ export default {
55
59
  default: () => ({}),
56
60
  },
57
61
 
58
- /** Dialog title. */
62
+ /** Dialog title; empty reads "Share view: {name}". */
59
63
  dialogTitle: {
60
64
  type: String,
61
- default: () => t('nextcloud-vue', 'Share view'),
65
+ default: '',
62
66
  },
63
67
  },
64
68
 
@@ -66,12 +70,19 @@ export default {
66
70
 
67
71
  data() {
68
72
  return {
69
- sharedWith: Array.isArray(this.view && this.view.sharedWith) ? this.view.sharedWith.map((e) => ({ group: e.group, mode: e.mode === 'write' ? 'write' : 'read' })) : [],
73
+ sharedWith: normalizeSharedWith(this.view && this.view.sharedWith),
70
74
  loading: false,
71
75
  error: '',
72
76
  }
73
77
  },
74
78
 
79
+ computed: {
80
+ /** @return {string} The title: `dialogTitle`, else "Share view: {name}". */
81
+ resolvedTitle() {
82
+ return this.dialogTitle || t('nextcloud-vue', 'Share view: {name}', { name: (this.view && this.view.name) || '' })
83
+ },
84
+ },
85
+
75
86
  methods: {
76
87
  t,
77
88
 
@@ -79,8 +90,8 @@ export default {
79
90
  this.loading = true
80
91
  this.error = ''
81
92
  /**
82
- * @event confirm Save clicked. Payload: the audience, `[{ group, mode }]` (`[]` to stop sharing).
83
- * @type {Array<{group: string, mode: string}>}
93
+ * @event confirm Save clicked. Payload: the audience, `[{ group, mode }]` and `[{ user, mode }]` (`[]` to stop sharing).
94
+ * @type {Array<{group?: string, user?: string, mode: string}>}
84
95
  */
85
96
  this.$emit('confirm', this.sharedWith)
86
97
  },
@@ -105,6 +116,10 @@ export default {
105
116
  </script>
106
117
 
107
118
  <style scoped>
119
+ .cn-share-view__intro {
120
+ margin: 0;
121
+ }
122
+
108
123
  .cn-share-view {
109
124
  display: flex;
110
125
  flex-direction: column;
@@ -5,7 +5,7 @@
5
5
  <template>
6
6
  <div v-if="available" class="cn-saved-view-share" data-testid="cn-saved-view-share">
7
7
  <NcSelect
8
- :inputLabel="t('nextcloud-vue', 'Share with groups')"
8
+ :inputLabel="t('nextcloud-vue', 'Share with')"
9
9
  :modelValue="selectedOptions"
10
10
  :options="options"
11
11
  :multiple="true"
@@ -16,32 +16,75 @@
16
16
  data-testid="cn-saved-view-share-groups"
17
17
  @search="onSearch"
18
18
  @update:modelValue="onGroups" />
19
- <ul v-if="modelValue.length > 0" class="cn-saved-view-share__modes">
20
- <li v-for="entry in modelValue" :key="entry.group" class="cn-saved-view-share__mode">
21
- <span class="cn-saved-view-share__group">{{ labelOf(entry.group) }}</span>
22
- <NcCheckboxRadioSwitch
23
- :modelValue="entry.mode === 'write'"
24
- :disabled="disabled"
25
- :data-testid="`cn-saved-view-share-mode-${entry.group}`"
26
- @update:modelValue="(on) => onMode(entry.group, on ? 'write' : 'read')">
27
- {{ t('nextcloud-vue', 'May edit') }}
28
- </NcCheckboxRadioSwitch>
29
- </li>
30
- </ul>
19
+ <p class="cn-saved-view-share__hint">
20
+ {{ t('nextcloud-vue', 'People and groups from your Nextcloud, as when sharing a file.') }}
21
+ </p>
22
+ <template v-if="modelValue.length > 0">
23
+ <h4 class="cn-saved-view-share__caption">
24
+ {{ t('nextcloud-vue', 'Shared with') }}
25
+ </h4>
26
+ <ul class="cn-saved-view-share__modes">
27
+ <li v-for="entry in modelValue" :key="keyOf(entry)" class="cn-saved-view-share__mode">
28
+ <span class="cn-saved-view-share__who">
29
+ <span class="cn-saved-view-share__group">{{ labelOf(keyOf(entry)) }}</span>
30
+ <span class="cn-saved-view-share__kind">{{ entry.user ? t('nextcloud-vue', 'Person') : t('nextcloud-vue', 'Group') }}</span>
31
+ </span>
32
+ <span
33
+ class="cn-saved-view-share__rights"
34
+ role="radiogroup"
35
+ :aria-label="t('nextcloud-vue', 'Rights of {name}', { name: labelOf(keyOf(entry)) })"
36
+ :data-testid="`cn-saved-view-share-mode-${keyOf(entry)}`">
37
+ <NcCheckboxRadioSwitch
38
+ type="radio"
39
+ :name="`cn-share-mode-${keyOf(entry)}`"
40
+ value="read"
41
+ :modelValue="entry.mode === 'write' ? 'write' : 'read'"
42
+ :disabled="disabled"
43
+ @update:modelValue="onMode(keyOf(entry), 'read')">
44
+ {{ t('nextcloud-vue', 'May use') }}
45
+ </NcCheckboxRadioSwitch>
46
+ <NcCheckboxRadioSwitch
47
+ type="radio"
48
+ :name="`cn-share-mode-${keyOf(entry)}`"
49
+ value="write"
50
+ :modelValue="entry.mode === 'write' ? 'write' : 'read'"
51
+ :disabled="disabled"
52
+ @update:modelValue="onMode(keyOf(entry), 'write')">
53
+ {{ t('nextcloud-vue', 'May update') }}
54
+ </NcCheckboxRadioSwitch>
55
+ </span>
56
+ <NcButton
57
+ variant="tertiary"
58
+ :aria-label="t('nextcloud-vue', 'Stop sharing with {name}', { name: labelOf(keyOf(entry)) })"
59
+ :disabled="disabled"
60
+ :data-testid="`cn-saved-view-share-remove-${keyOf(entry)}`"
61
+ @click="onRemove(keyOf(entry))">
62
+ <template #icon>
63
+ <Close :size="20" />
64
+ </template>
65
+ </NcButton>
66
+ </li>
67
+ </ul>
68
+ <p class="cn-saved-view-share__hint">
69
+ {{ t('nextcloud-vue', 'May use: sees the view under Shared with me. May update: can also replace the criteria. Deleting and sharing stay with you.') }}
70
+ </p>
71
+ </template>
31
72
  </div>
32
73
  </template>
33
74
 
34
75
  <script>
35
76
  import { translate as t } from '@nextcloud/l10n'
36
- import { NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'
37
- import { searchGroupSharees } from '../../utils/searchGroupSharees.js'
77
+ import { NcButton, NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'
78
+ import Close from 'vue-material-design-icons/Close.vue'
79
+ import { searchUserAndGroupSharees } from '../../utils/searchGroupSharees.js'
38
80
 
39
81
  /**
40
- * CnSavedViewShareFields — the sharing section of a saved view: pick groups
41
- * from Nextcloud's sharee API and choose, per group, whether members may only
42
- * read the view or also edit it. Used by `CnSaveViewDialog` and the share
43
- * dialog of `CnIndexPage`. Renders nothing when the sharee API answers no
44
- * groups (sharing off, or no group the user may share with).
82
+ * CnSavedViewShareFields — the sharing section of a saved view: pick people
83
+ * and groups from Nextcloud's sharee API, within the instance's own sharing
84
+ * settings, and choose per entry whether it may only read the view or also
85
+ * edit it. Used by `CnSaveViewDialog` and the share dialog of `CnIndexPage`.
86
+ * Renders nothing when the sharee API answers nobody (sharing off, or nobody
87
+ * the user may share with). A group is keyed by its id, a user by `user:<uid>`.
45
88
  *
46
89
  * Example:
47
90
  * ```vue
@@ -51,13 +94,14 @@ import { searchGroupSharees } from '../../utils/searchGroupSharees.js'
51
94
  export default {
52
95
  name: 'CnSavedViewShareFields',
53
96
 
54
- components: { NcCheckboxRadioSwitch, NcSelect },
97
+ components: { Close, NcButton, NcCheckboxRadioSwitch, NcSelect },
55
98
 
56
99
  props: {
57
100
  /**
58
- * The chosen audience, `[{ group, mode }]` with `mode` `read` or `write`.
101
+ * The chosen audience, `[{ group, mode }]` and `[{ user, mode }]` with
102
+ * `mode` `read` or `write`.
59
103
  *
60
- * @type {Array<{group: string, mode: 'read'|'write'}>}
104
+ * @type {Array<{group?: string, user?: string, mode: 'read'|'write'}>}
61
105
  */
62
106
  modelValue: {
63
107
  type: Array,
@@ -78,14 +122,14 @@ export default {
78
122
  options: [],
79
123
  known: {},
80
124
  loading: false,
81
- /** False until the first search came back with at least one group. */
125
+ /** False until the first search came back with at least one sharee. */
82
126
  available: false,
83
127
  }
84
128
  },
85
129
 
86
130
  computed: {
87
131
  selectedOptions() {
88
- return this.modelValue.map((e) => ({ id: e.group, label: this.labelOf(e.group) }))
132
+ return this.modelValue.map((e) => ({ id: this.keyOf(e), label: this.labelOf(this.keyOf(e)) }))
89
133
  },
90
134
  },
91
135
 
@@ -100,17 +144,28 @@ export default {
100
144
  methods: {
101
145
  t,
102
146
 
103
- labelOf(group) {
104
- return this.known[group] || group
147
+ /**
148
+ * The key an audience entry or option goes by: the group id, or `user:<uid>`.
149
+ *
150
+ * @param {{group?: string, user?: string}} entry The entry.
151
+ * @return {string}
152
+ * @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-5
153
+ */
154
+ keyOf(entry) {
155
+ return typeof entry.user === 'string' && entry.user !== '' ? `user:${entry.user}` : entry.group
156
+ },
157
+
158
+ labelOf(key) {
159
+ return this.known[key] || (key.startsWith('user:') ? key.slice(5) : key)
105
160
  },
106
161
 
107
162
  async onSearch(query) {
108
163
  this.loading = true
109
- const found = await searchGroupSharees(query || '')
164
+ const found = await searchUserAndGroupSharees(query || '')
110
165
  this.loading = false
111
- this.options = found
112
- found.forEach((g) => {
113
- this.known = { ...this.known, [g.id]: g.label }
166
+ this.options = found.map((s) => ({ id: s.kind === 'user' ? `user:${s.id}` : s.id, label: s.label }))
167
+ this.options.forEach((o) => {
168
+ this.known = { ...this.known, [o.id]: o.label }
114
169
  })
115
170
  if (found.length > 0) {
116
171
  this.available = true
@@ -120,15 +175,27 @@ export default {
120
175
  onGroups(list) {
121
176
  const groups = Array.isArray(list) ? list : []
122
177
  const next = groups.map((g) => {
123
- const existing = this.modelValue.find((e) => e.group === g.id)
124
- return existing || { group: g.id, mode: 'read' }
178
+ const existing = this.modelValue.find((e) => this.keyOf(e) === g.id)
179
+ if (existing) {
180
+ return existing
181
+ }
182
+ return g.id.startsWith('user:') ? { user: g.id.slice(5), mode: 'read' } : { group: g.id, mode: 'read' }
125
183
  })
126
- /** @event update:modelValue The audience, `[{ group, mode }]`; `[]` when no group is picked. */
184
+ /** @event update:modelValue The audience, `[{ group, mode }]` and `[{ user, mode }]`; `[]` when nobody is picked. */
127
185
  this.$emit('update:modelValue', next)
128
186
  },
129
187
 
130
- onMode(group, mode) {
131
- this.$emit('update:modelValue', this.modelValue.map((e) => (e.group === group ? { ...e, mode } : e)))
188
+ /**
189
+ * Stop sharing with one person or group.
190
+ *
191
+ * @param {string} key The entry's key.
192
+ */
193
+ onRemove(key) {
194
+ this.$emit('update:modelValue', this.modelValue.filter((e) => this.keyOf(e) !== key))
195
+ },
196
+
197
+ onMode(key, mode) {
198
+ this.$emit('update:modelValue', this.modelValue.map((e) => (this.keyOf(e) === key ? { ...e, mode } : e)))
132
199
  },
133
200
  },
134
201
  }
@@ -145,6 +212,32 @@ export default {
145
212
  display: flex;
146
213
  align-items: center;
147
214
  justify-content: space-between;
148
- gap: 12px;
215
+ flex-wrap: wrap;
216
+ gap: 8px 12px;
217
+ }
218
+
219
+ .cn-saved-view-share__who {
220
+ display: flex;
221
+ flex-direction: column;
222
+ flex: 1 1 160px;
223
+ min-width: 0;
224
+ }
225
+
226
+ .cn-saved-view-share__kind,
227
+ .cn-saved-view-share__hint {
228
+ margin: 0;
229
+ font-size: var(--font-size-small, 13px);
230
+ color: var(--color-text-maxcontrast);
231
+ }
232
+
233
+ .cn-saved-view-share__rights {
234
+ display: flex;
235
+ gap: 8px;
236
+ }
237
+
238
+ .cn-saved-view-share__caption {
239
+ margin: 8px 0 0;
240
+ font-size: var(--default-font-size);
241
+ font-weight: bold;
149
242
  }
150
243
  </style>