@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
@@ -4,10 +4,10 @@
4
4
  -->
5
5
  <template>
6
6
  <!--
7
- Board look: the saved views are a row of chips on the ground, each with
8
- its count, the selected one filled. The menu stays for managing views
9
- (apply, rename, share, delete) and is reached from the "Save view"
10
- button. Without the look the control is the menu alone, as before.
7
+ The toolbar half of saved views. Under the board look the views are
8
+ a row of chips that apply them; everywhere a "Save view" button saves
9
+ the current filters. Listing, editing, sharing and deleting live in the
10
+ sidebar's Filters tab (CnSavedViewsPanel).
11
11
  -->
12
12
  <div
13
13
  v-if="isBoardLook && chipRows.length > 0"
@@ -20,8 +20,8 @@
20
20
  :key="`chip-${row.view.id || row.view.slug}`"
21
21
  type="button"
22
22
  class="cn-saved-views__chip"
23
- :class="{ 'cn-saved-views__chip--selected': isChipSelected(row.view) }"
24
- :aria-pressed="isChipSelected(row.view)"
23
+ :class="{ 'cn-saved-views__chip--selected': isSelected(row.view) }"
24
+ :aria-pressed="isSelected(row.view)"
25
25
  data-testid="cn-saved-views-chip"
26
26
  :data-view-id="row.view.id || row.view.slug"
27
27
  @click="onApply(row.view)">
@@ -32,645 +32,82 @@
32
32
  data-testid="cn-saved-views-chip-count">{{ countOf(row.view) }}</span>
33
33
  </button>
34
34
  </div>
35
- <NcActions
36
- v-bind="$attrs"
37
- :class="{ 'cn-saved-views__save': isBoardLook }"
38
- :forceMenu="true"
39
- :forceName="true"
40
- :menuName="isBoardLook ? t('nextcloud-vue', 'Save view') : menuLabel"
41
- data-testid="cn-saved-views-control"
42
- :aria-label="isBoardLook ? t('nextcloud-vue', 'Save view') : menuLabel">
43
- <template #icon>
44
- <ContentSaveOutline v-if="isBoardLook" :size="14" />
45
- <BookmarkOutline v-else :size="20" />
46
- </template>
47
-
48
- <NcActionCaption :name="t('nextcloud-vue', 'Saved views')" />
49
-
50
- <!-- Loading state -->
51
- <NcActionCaption
52
- v-if="loading"
53
- data-testid="cn-saved-views-loading"
54
- :name="t('nextcloud-vue', 'Loading…')" />
55
-
56
- <!-- Empty state -->
57
- <NcActionCaption
58
- v-else-if="views.length === 0"
59
- data-testid="cn-saved-views-empty"
60
- :name="t('nextcloud-vue', 'No saved views yet')" />
61
-
62
- <!-- The labels already in use, offered before anybody types a new one. -->
63
- <template v-if="!loading && offeredLabels.length > 0">
64
- <NcActionButton
65
- v-for="label in offeredLabels"
66
- :key="`label-${label}`"
67
- data-testid="cn-saved-views-label"
68
- :data-label="label"
69
- :modelValue="labelFilter === label"
70
- type="checkbox"
71
- :aria-label="labelFilterLabel(label)"
72
- @click="onLabelFilter(label)">
73
- <template #icon>
74
- <TagOutline :size="20" />
75
- </template>
76
- {{ label }}
77
- </NcActionButton>
78
- <NcActionSeparator />
79
- </template>
80
-
81
- <!--
82
- A label that matches nothing says so. Chained to the ROWS rather
83
- than to the label list: written as an else-if of the labels block
84
- it only ever rendered on a page with no labels at all, which is
85
- the one case it cannot be about.
86
- -->
87
- <NcActionCaption
88
- v-if="!loading && views.length > 0 && rows.length === 0"
89
- data-testid="cn-saved-views-none-for-label"
90
- :name="t('nextcloud-vue', 'No views with this label')" />
91
-
92
- <!-- One row per view, in tree order: the name applies it, a trailing
93
- icon button deletes it. NcActions only recognises NcAction* vnodes
94
- as menu items and silently drops anything else, so a row with more
95
- than one control has to be an NcActionButtonGroup. -->
96
- <template v-if="!loading">
97
- <template v-for="section in sections" :key="section.key">
98
- <NcActionCaption
99
- v-if="section.label"
100
- :data-testid="`cn-saved-views-section-${section.key}`"
101
- :name="section.label" />
102
- <NcActionButtonGroup
103
- v-for="row in section.rows"
104
- :key="`view-${row.view.id || row.view.slug}`"
105
- class="cn-saved-view-row">
106
- <NcActionButton
107
- data-testid="cn-saved-views-item"
108
- :data-view-id="row.view.id || row.view.slug"
109
- :data-depth="row.depth"
110
- :data-group="row.group"
111
- :data-access="accessOf(row)"
112
- :aria-label="rowLabel(row)"
113
- @click="onApply(row.view)">
114
- <template #icon>
115
- <EyeOutline :size="20" />
116
- </template>
117
- {{ rowName(row, section.key === 'shared') }}
118
- </NcActionButton>
119
- <NcActionButton
120
- v-if="accessOf(row) === 'owner' && row.group !== SEEDED_GROUP"
121
- :key="`share-${row.view.id || row.view.slug}`"
122
- data-testid="cn-saved-views-share"
123
- :data-view-id="row.view.id || row.view.slug"
124
- :aria-label="shareLabel(row.view)"
125
- @click="onShareRequest(row.view)">
126
- <template #icon>
127
- <ShareVariantOutline :size="20" />
128
- </template>
129
- </NcActionButton>
130
- <NcActionButton
131
- v-if="(accessOf(row) === 'owner' || accessOf(row) === 'write') && row.group !== SEEDED_GROUP"
132
- :key="`presentation-${row.view.id || row.view.slug}`"
133
- data-testid="cn-saved-views-presentation"
134
- :data-view-id="row.view.id || row.view.slug"
135
- :aria-label="presentationLabel(row.view)"
136
- @click="onPresentationRequest(row.view)">
137
- <template #icon>
138
- <ViewDashboardOutline :size="20" />
139
- </template>
140
- </NcActionButton>
141
- <NcActionButton
142
- v-if="accessOf(row) === 'write'"
143
- :key="`update-${row.view.id || row.view.slug}`"
144
- data-testid="cn-saved-views-update"
145
- :data-view-id="row.view.id || row.view.slug"
146
- :aria-label="updateLabel(row.view)"
147
- @click="onUpdateRequest(row.view)">
148
- <template #icon>
149
- <ContentSaveOutline :size="20" />
150
- </template>
151
- </NcActionButton>
152
- <NcActionButton
153
- v-if="accessOf(row) === 'read' && row.group !== SEEDED_GROUP"
154
- :key="`copy-${row.view.id || row.view.slug}`"
155
- data-testid="cn-saved-views-copy"
156
- :data-view-id="row.view.id || row.view.slug"
157
- :aria-label="copyLabel(row.view)"
158
- @click="onCopyRequest(row.view)">
159
- <template #icon>
160
- <ContentCopy :size="20" />
161
- </template>
162
- </NcActionButton>
163
- <NcActionButton
164
- v-if="accessOf(row) === 'owner' && row.group !== SEEDED_GROUP"
165
- :key="`delete-${row.view.id || row.view.slug}`"
166
- data-testid="cn-saved-views-delete"
167
- :data-view-id="row.view.id || row.view.slug"
168
- :aria-label="deleteLabel(row.view)"
169
- @click="onDeleteRequest(row.view)">
170
- <template #icon>
171
- <TrashCanOutline :size="20" />
172
- </template>
173
- </NcActionButton>
174
- </NcActionButtonGroup>
175
- </template>
176
- </template>
177
-
178
- <NcActionSeparator />
179
-
180
- <NcActionButton
181
- data-testid="cn-saved-views-save"
182
- :aria-label="t('nextcloud-vue', 'Save current view…')"
35
+ <!-- The wrapper carries the board look's class, so the bar's row rules
36
+ and the ghost-button rules reach the button the way they did when it
37
+ was an NcActions trigger. -->
38
+ <div v-bind="$attrs" :class="{ 'cn-saved-views__save': isBoardLook }">
39
+ <NcButton
40
+ :variant="isBoardLook ? 'tertiary' : 'secondary'"
41
+ :aria-label="t('nextcloud-vue', 'Save view')"
42
+ data-testid="cn-saved-views-control"
183
43
  @click="onSaveRequest">
184
44
  <template #icon>
185
- <ContentSaveOutline :size="20" />
45
+ <ContentSaveOutline :size="isBoardLook ? 14 : 20" />
186
46
  </template>
187
- {{ t('nextcloud-vue', 'Save current view…') }}
188
- </NcActionButton>
189
- </NcActions>
47
+ {{ t('nextcloud-vue', 'Save view') }}
48
+ </NcButton>
49
+ </div>
190
50
  </template>
191
51
 
192
52
  <script>
193
- import { translate as t } from '@nextcloud/l10n'
194
- import { NcActionButton, NcActionButtonGroup, NcActionCaption, NcActions, NcActionSeparator } from '@nextcloud/vue'
195
- import BookmarkOutline from 'vue-material-design-icons/BookmarkOutline.vue'
196
- import ContentCopy from 'vue-material-design-icons/ContentCopy.vue'
53
+ import { NcButton } from '@nextcloud/vue'
197
54
  import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue'
198
- import EyeOutline from 'vue-material-design-icons/EyeOutline.vue'
199
- import ShareVariantOutline from 'vue-material-design-icons/ShareVariantOutline.vue'
200
- import TagOutline from 'vue-material-design-icons/TagOutline.vue'
201
- import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
202
- import ViewDashboardOutline from 'vue-material-design-icons/ViewDashboardOutline.vue'
203
55
  import { normalizeLook } from '../../composables/useLook.js'
204
- import { buildViewTree, labelsInUse, VIEW_GROUPS } from '../../utils/buildViewTree.js'
205
- import { isOwnView, viewAccess } from '../../utils/savedViewHelpers.js'
56
+ import { savedViewsList } from '../../mixins/savedViewsList.js'
206
57
 
207
58
  /**
208
- * CnSavedViewsControl — toolbar dropdown listing OpenRegister saved-search
209
- * views for the current index page (saved-views-ui).
210
- *
211
- * Purely presentational: the parent (CnIndexPage) owns fetching via
212
- * `useSavedViewsApi()` and all mutations; this control only lists the
213
- * given `views` and emits intents:
59
+ * CnSavedViewsControl — the toolbar entry to saved views: a "Save view"
60
+ * button and, under the board look, the saved views as chips that apply
61
+ * them. Everything else (the list, edit, share, delete, add to menu) lives in
62
+ * the index sidebar's `CnSavedViewsPanel`.
214
63
  *
215
- * - `@apply(view)` — a view entry was clicked; parent writes the view's
216
- * stored filters/search/sort into the route query.
217
- * - `@save-request()` — "Save current view…" clicked; parent opens
218
- * CnSaveViewDialog.
219
- * - `@delete-request(view)` — a view's trailing delete icon clicked; parent
220
- * opens a confirm dialog. Only rendered for views the current user owns
221
- * (`@self.access: owner`, else `view.owner === currentUserId`) — OpenRegister
222
- * refuses foreign deletes server-side anyway (owner-scoped 404).
223
- * - `@share-request(view)` — Share on an own view; parent opens the share dialog.
224
- * - `@update-request(view)` — Save on a view shared with write access; parent
225
- * saves the current state to it (never sending `sharedWith` or `owner`).
226
- * - `@presentation-request(view)` — Presentation on a view the user may edit
227
- * (`owner` or `write`); parent opens the presentation dialog (table, board, calendar).
228
- * - `@copy-request(view)` — "Save as my view" on a view shared read-only.
64
+ * Purely presentational: `CnIndexPage` owns fetching and every write.
229
65
  *
230
- * Views shared with the user (`@self.access` `write` or `read`) list under
231
- * "Shared with me", next to "My views", once any exist; with none the list
232
- * renders as before.
233
- *
234
- * @event {object} apply — Apply the clicked view. Payload: the View API object.
235
- * @event {void} save-request — Open the save-current-view dialog.
236
- * @event {object} delete-request — Confirm-delete the clicked view. Payload: the View API object.
237
- * @event {object} share-request — Share the clicked own view. Payload: the View API object.
238
- * @event {object} update-request — Save the current state to the clicked writable shared view. Payload: the View API object.
239
- * @event {object} presentation-request — Change how the clicked view shows. Payload: the View API object.
240
- * @event {object} copy-request — Copy the clicked read-only shared view into a personal one. Payload: the View API object.
66
+ * @event {object} apply — A chip was clicked; apply its view. Payload: the View API object.
67
+ * @event {void} save-request — "Save view" clicked; open the save dialog.
241
68
  */
242
69
  export default {
243
70
  name: 'CnSavedViewsControl',
244
71
 
245
- components: {
246
- NcActions,
247
- NcActionButton,
248
- NcActionButtonGroup,
249
- NcActionCaption,
250
- NcActionSeparator,
251
- BookmarkOutline,
252
- ContentCopy,
253
- ContentSaveOutline,
254
- EyeOutline,
255
- ShareVariantOutline,
256
- TagOutline,
257
- TrashCanOutline,
258
- ViewDashboardOutline,
259
- },
72
+ components: { NcButton, ContentSaveOutline },
73
+
74
+ mixins: [savedViewsList],
260
75
 
261
76
  inject: {
262
77
  /** The look CnAppRoot provides; `board` draws the views as chips. */
263
78
  cnLook: { default: 'nextcloud' },
264
79
  },
265
80
 
266
- // The chips and the menu are two roots under the board look.
81
+ // The chips and the button are two roots under the board look.
267
82
  inheritAttrs: false,
268
83
 
269
- props: {
270
- /** Views to list (View API objects from `GET /apps/openregister/api/views`). */
271
- views: {
272
- type: Array,
273
- default: () => [],
274
- },
275
-
276
- /** True while the parent is fetching the view list. */
277
- loading: {
278
- type: Boolean,
279
- default: false,
280
- },
281
-
282
- /** The signed-in NC user id — gates the per-view delete affordance. */
283
- currentUserId: {
284
- type: String,
285
- default: '',
286
- },
287
-
288
- /**
289
- * How deep the tree indents before it flattens. Mirrors
290
- * `savedViewTree.maxDepth` in the manifest. Flattening is about
291
- * indentation only: a view past the bound still renders.
292
- */
293
- maxDepth: {
294
- type: Number,
295
- default: 3,
296
- },
297
-
298
- /**
299
- * How many records each view matches, keyed by view id (or slug). A
300
- * view with a number here shows it after its name; a view can also
301
- * carry its own `count`. `null` (the default) shows no counts.
302
- *
303
- * @type {{[viewId: string]: number}|null}
304
- */
305
- counts: {
306
- type: Object,
307
- default: null,
308
- },
309
-
310
- /**
311
- * The id (or slug) of the applied view; its chip renders selected under
312
- * the board look.
313
- */
314
- selectedViewId: {
315
- type: String,
316
- default: '',
317
- },
318
- },
319
-
320
- emits: ['apply', 'copy-request', 'delete-request', 'presentation-request', 'save-request', 'share-request', 'update-request'],
321
-
322
- data() {
323
- return {
324
- /** The label currently filtering the list, or '' for all of them. */
325
- labelFilter: '',
326
- }
327
- },
84
+ emits: ['apply', 'save-request'],
328
85
 
329
86
  computed: {
330
- /**
331
- * Whether the saved views draw as chips (the board look).
332
- *
333
- * @return {boolean}
334
- */
87
+ /** @return {boolean} Whether the saved views draw as chips. */
335
88
  isBoardLook() {
336
89
  return normalizeLook(this.cnLook) === 'board'
337
90
  },
338
91
 
339
- /**
340
- * The views that get a chip: the top level of the tree, in tree order.
341
- *
342
- * @return {Array<object>}
343
- */
92
+ /** @return {Array<object>} The top level of the tree, in tree order. */
344
93
  chipRows() {
345
94
  return this.rows.filter((row) => row.depth === 0)
346
95
  },
347
-
348
- /** @return {string} The dropdown trigger label. */
349
- menuLabel() {
350
- return t('nextcloud-vue', 'Views')
351
- },
352
-
353
- /** @return {string} The seeded group name, for the template. */
354
- SEEDED_GROUP() {
355
- return VIEW_GROUPS.SEEDED
356
- },
357
-
358
- /**
359
- * The views in tree order, seeded first, filtered by the active label.
360
- *
361
- * @return {Array<object>} Rows of `{ view, depth, group, orphaned }`.
362
- */
363
- rows() {
364
- return buildViewTree({
365
- views: this.views,
366
- labelFilter: this.labelFilter,
367
- maxDepth: this.maxDepth,
368
- })
369
- },
370
-
371
- /**
372
- * The rows in their sections. With no view shared with this user there
373
- * is one section without a caption, as the list always was. With
374
- * shared views: seeded views (no caption), "My views", "Shared with me".
375
- *
376
- * @return {Array<{key: string, label: string, rows: Array<object>}>} The sections.
377
- * @spec openspec/changes/saved-views-shared-by-role/tasks.md#task-1
378
- */
379
- sections() {
380
- const shared = this.rows.filter((r) => r.group !== VIEW_GROUPS.SEEDED && this.accessOf(r) !== 'owner')
381
- if (shared.length === 0) {
382
- return [{ key: 'all', label: '', rows: this.rows }]
383
- }
384
- const seeded = this.rows.filter((r) => r.group === VIEW_GROUPS.SEEDED)
385
- const mine = this.rows.filter((r) => r.group !== VIEW_GROUPS.SEEDED && this.accessOf(r) === 'owner')
386
- return [
387
- { key: 'seeded', label: '', rows: seeded },
388
- { key: 'mine', label: t('nextcloud-vue', 'My views'), rows: mine },
389
- { key: 'shared', label: t('nextcloud-vue', 'Shared with me'), rows: shared },
390
- ].filter((section) => section.rows.length > 0)
391
- },
392
-
393
- /**
394
- * The labels already in use.
395
- *
396
- * Read from the UNFILTERED list on purpose: reading it from `rows`
397
- * would remove every other label from the menu the moment one was
398
- * chosen, and a filter you cannot change without clearing it first is
399
- * a filter people stop using.
400
- *
401
- * @return {Array<string>} The labels.
402
- */
403
- offeredLabels() {
404
- return labelsInUse(this.views)
405
- },
406
96
  },
407
97
 
408
98
  methods: {
409
- t,
410
-
411
- /**
412
- * Whether a view's chip is the selected one.
413
- *
414
- * @param {object} view The view.
415
- * @return {boolean}
416
- */
417
- isChipSelected(view) {
418
- return this.selectedViewId !== '' && (this.selectedViewId === String(view.id) || this.selectedViewId === String(view.slug))
419
- },
420
-
421
- /**
422
- * The name a row shows, indented to its depth.
423
- *
424
- * Indented with figure spaces rather than CSS padding because the row
425
- * is an `NcActionButton` whose label is read out as text: a screen
426
- * reader gets the same shape a sighted reader does, and no stylesheet
427
- * has to know about the tree.
428
- *
429
- * @param {object} row The row from buildViewTree().
430
- * @param {boolean} [withGroups] Append the groups the view is shared with.
431
- * @return {string} The name.
432
- * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views
433
- */
434
- rowName(row, withGroups = false) {
435
- const groups = withGroups ? this.groupsOf(row.view) : ''
436
- return `${'\u2007'.repeat(row.depth * 2)}${this.nameWithCount(row.view)}${groups !== '' ? ` · ${groups}` : ''}`
437
- },
438
-
439
- /**
440
- * What the current user may do with a row's view.
441
- *
442
- * @param {object} row The row from buildViewTree().
443
- * @return {'owner'|'write'|'read'} The access.
444
- * @spec openspec/changes/saved-views-shared-by-role/tasks.md#task-1
445
- */
446
- accessOf(row) {
447
- return viewAccess(row.view, this.currentUserId)
448
- },
449
-
450
- /**
451
- * The groups a view is shared with, as text for the row.
452
- *
453
- * @param {object} view The View API object.
454
- * @return {string} The group ids, comma-separated; empty when none.
455
- */
456
- groupsOf(view) {
457
- return (Array.isArray(view.sharedWith) ? view.sharedWith : [])
458
- .map((e) => e && e.group)
459
- .filter((g) => typeof g === 'string' && g !== '')
460
- .join(', ')
461
- },
462
-
463
- shareLabel(view) {
464
- return t('nextcloud-vue', 'Share "{name}"', { name: view.name })
465
- },
466
-
467
- updateLabel(view) {
468
- return t('nextcloud-vue', 'Save the current view to "{name}"', { name: view.name })
469
- },
470
-
471
- copyLabel(view) {
472
- return t('nextcloud-vue', 'Save "{name}" as my view', { name: view.name })
473
- },
474
-
475
- presentationLabel(view) {
476
- return t('nextcloud-vue', 'Change how "{name}" shows', { name: view.name })
477
- },
478
-
479
- onPresentationRequest(view) {
480
- /**
481
- * @event presentation-request Presentation on an editable view; open the presentation dialog.
482
- * @type {object}
483
- */
484
- this.$emit('presentation-request', view)
485
- },
486
-
487
- onShareRequest(view) {
488
- /**
489
- * @event share-request Share on an own view; open the share dialog.
490
- * @type {object}
491
- */
492
- this.$emit('share-request', view)
493
- },
494
-
495
- onUpdateRequest(view) {
496
- /**
497
- * @event update-request Save on a writable shared view; save the current state to it.
498
- * @type {object}
499
- */
500
- this.$emit('update-request', view)
501
- },
502
-
503
- onCopyRequest(view) {
504
- /**
505
- * @event copy-request "Save as my view" on a read-only shared view; store a personal copy.
506
- * @type {object}
507
- */
508
- this.$emit('copy-request', view)
509
- },
510
-
511
- /**
512
- * The number of records a view matches, or null when none is known:
513
- * `counts[id]` (or `counts[slug]`) first, else the view's own `count`.
514
- *
515
- * @param {object} view The saved view.
516
- * @return {number|null}
517
- * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views
518
- */
519
- countOf(view) {
520
- const counts = this.counts || {}
521
- let value = counts[view.id]
522
- if (value === undefined || value === null) {
523
- value = counts[view.slug]
524
- }
525
- if (value === undefined || value === null) {
526
- value = view.count
527
- }
528
- return (typeof value === 'number' && Number.isFinite(value)) ? value : null
529
- },
530
-
531
- /**
532
- * A view's name, followed by its count when one is known.
533
- *
534
- * @param {object} view The saved view.
535
- * @return {string}
536
- * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views
537
- */
538
- nameWithCount(view) {
539
- const count = this.countOf(view)
540
- return count === null ? view.name : `${view.name} (${count})`
541
- },
542
-
543
- /**
544
- * What a row is called to somebody who cannot see the indentation.
545
- *
546
- * Says the level in words, and says when a view's parent is one this
547
- * reader cannot see. A child that silently sits at the root looks
548
- * exactly like a view that never had a parent.
549
- *
550
- * @param {object} row The row from buildViewTree().
551
- * @return {string} The accessible label.
552
- * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views
553
- */
554
- rowLabel(row) {
555
- const viewName = this.nameWithCount(row.view)
556
- const name = row.depth === 0
557
- ? viewName
558
- : t('nextcloud-vue', '{name}, level {level}', { name: viewName, level: row.depth + 1 })
559
-
560
- if (row.orphaned !== true) {
561
- return name
562
- }
563
-
564
- return t('nextcloud-vue', '{name}. Parts of this view come from one you cannot see.', { name })
565
- },
566
-
567
- /**
568
- * The accessible label of a label filter entry.
569
- *
570
- * @param {string} label The label.
571
- * @return {string} The label.
572
- */
573
- labelFilterLabel(label) {
574
- return this.labelFilter === label
575
- ? t('nextcloud-vue', 'Stop filtering on "{label}"', { label })
576
- : t('nextcloud-vue', 'Show only views labelled "{label}"', { label })
577
- },
578
-
579
- /**
580
- * Turn a label filter on, or off when it is already on.
581
- *
582
- * Clicking the active label clears it, so the way out is the way in
583
- * rather than a second control somebody has to find.
584
- *
585
- * @param {string} label The label.
586
- */
587
- onLabelFilter(label) {
588
- this.labelFilter = this.labelFilter === label ? '' : label
589
- },
590
-
591
- /**
592
- * Whether the delete entry renders for a view.
593
- *
594
- * @param {object} view The View API object.
595
- * @return {boolean} True when the current user owns the view.
596
- */
597
- isOwn(view) {
598
- return isOwnView(view, this.currentUserId)
599
- },
600
-
601
- /**
602
- * Accessible label for a view's delete entry.
603
- *
604
- * @param {object} view The View API object.
605
- * @return {string} The label.
606
- */
607
- deleteLabel(view) {
608
- return t('nextcloud-vue', 'Delete "{name}"', { name: view.name })
609
- },
610
-
611
- /**
612
- * View-entry click: hand the view to the parent to apply.
613
- *
614
- * @param {object} view The clicked View API object.
615
- */
616
99
  onApply(view) {
617
100
  /**
618
- * @event apply A view entry was clicked; apply its stored state.
101
+ * @event apply A chip was clicked; apply its stored state.
619
102
  * @type {object}
620
103
  */
621
104
  this.$emit('apply', view)
622
105
  },
623
106
 
624
- /**
625
- * "Save current view…" click: ask the parent to open the save dialog.
626
- */
627
107
  onSaveRequest() {
628
- /**
629
- * @event save-request "Save current view…" clicked; open the save dialog.
630
- */
108
+ /** @event save-request "Save view" clicked; open the save dialog. */
631
109
  this.$emit('save-request')
632
110
  },
633
-
634
- /**
635
- * Delete-entry click: hand the view to the parent to confirm-delete.
636
- *
637
- * @param {object} view The View API object to delete.
638
- */
639
- onDeleteRequest(view) {
640
- /**
641
- * @event delete-request A view's delete entry was clicked; confirm and delete.
642
- * @type {object}
643
- */
644
- this.$emit('delete-request', view)
645
- },
646
111
  },
647
112
  }
648
113
  </script>
649
-
650
- <style scoped>
651
- /* NcActionButtonGroup gives every child `flex: 1 1` (equal width) by
652
- default; override so the name button grows and the delete icon stays
653
- compact. :deep() is required — these li's belong to NcActionButtonGroup's
654
- own scope, not this component's. */
655
- .cn-saved-view-row :deep(.nc-button-group-content) {
656
- gap: 0;
657
- }
658
-
659
- /* Anchored on the FIRST child, not the last: a row carries one or two buttons
660
- depending on ownership, so "the last one" is the name button on a row that
661
- has only that. */
662
- .cn-saved-view-row :deep(.nc-button-group-content > li) {
663
- flex: 0 0 auto;
664
- }
665
-
666
- .cn-saved-view-row :deep(.nc-button-group-content > li:first-child) {
667
- flex: 1 1 auto;
668
- }
669
-
670
- /* NcActionButtonGroup also centers .action-button content (fine for an
671
- icon-only toolbar button, wrong for a row with a name) — restore the
672
- normal left-aligned NcActionButton look for the name button. */
673
- .cn-saved-view-row :deep(.nc-button-group-content > li:first-child .action-button) {
674
- justify-content: flex-start;
675
- }
676
- </style>