@conduction/nextcloud-vue 2.141.0 → 2.143.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 (140) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +27 -0
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js +2 -1
  8. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAiCompanion/CnAiInput.vue2.js +9 -0
  10. package/dist/esm/components/CnAiCompanion/CnAiInput.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAiCompanion/CnAiInput.vue3.js +21 -22
  12. package/dist/esm/components/CnAiCompanion/CnAiInput.vue3.js.map +1 -1
  13. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  14. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +29 -1
  15. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  16. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js +28 -13
  17. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  18. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  19. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  20. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +2 -1
  21. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  22. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  23. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +13 -0
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  25. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +3 -3
  26. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  27. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  28. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  30. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  31. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  32. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +21 -0
  34. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -1
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  38. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  42. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +34 -3
  43. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  46. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +70 -5
  47. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +5 -3
  49. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  50. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +17 -4
  51. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  52. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +21 -1
  53. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +36 -2
  55. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  56. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +14 -0
  59. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  60. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  61. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +3 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  65. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +6 -1
  66. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -1
  67. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -1
  68. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -1
  69. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +31 -1
  70. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  71. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  72. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  73. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +12 -1
  74. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +2 -2
  76. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  77. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +1 -1
  78. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -1
  80. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +1 -1
  81. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +4 -0
  82. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -1
  83. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +18 -8
  84. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -1
  85. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue.js +1 -1
  86. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js +52 -1
  87. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js.map +1 -1
  88. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js +119 -65
  89. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js.map +1 -1
  90. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
  91. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +86 -4
  92. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
  93. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +67 -5
  94. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +10 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +12 -3
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/library-style.js +1 -1
  100. package/dist/esm/nextcloud-vue.css +976 -592
  101. package/dist/esm/utils/buildViewTree.js +23 -8
  102. package/dist/esm/utils/buildViewTree.js.map +1 -1
  103. package/dist/nextcloud-vue.cjs.js +1363 -734
  104. package/dist/nextcloud-vue.cjs.js.map +2 -2
  105. package/dist/nextcloud-vue.css +976 -592
  106. package/l10n/en.json +10 -1
  107. package/l10n/nl.json +12 -3
  108. package/package.json +1 -1
  109. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +28 -1
  110. package/src/components/CnAiCompanion/CnAiChatPanel.vue +12 -0
  111. package/src/components/CnAiCompanion/CnAiInput.vue +32 -1
  112. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +34 -4
  113. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +1 -0
  114. package/src/components/CnCellRenderer/CnCellRenderer.vue +15 -1
  115. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +8 -0
  116. package/src/components/CnChoiceCards/CnChoiceCards.vue +1 -0
  117. package/src/components/CnDataTable/CnDataTable.md +1 -0
  118. package/src/components/CnDataTable/CnDataTable.vue +22 -1
  119. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +1 -0
  120. package/src/components/CnFactsWidget/CnFactsWidget.vue +34 -3
  121. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +72 -7
  122. package/src/components/CnFilesBrowser/filesBrowser.js +17 -4
  123. package/src/components/CnFilesBrowser/filesBrowserColumns.js +20 -1
  124. package/src/components/CnIndexPage/CnIndexPage.vue +37 -2
  125. package/src/components/CnIndexPage/useSelfFetchList.js +14 -0
  126. package/src/components/CnObjectSidebar/CnFilesTab.vue +4 -1
  127. package/src/components/CnPageHeader/CnHeaderTrail.vue +6 -1
  128. package/src/components/CnPartyPicker/CnPartyPicker.vue +31 -1
  129. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +21 -1
  130. package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +6 -0
  131. package/src/components/CnTimelineWidget/CnTimelineManage.vue +10 -1
  132. package/src/css/dialog.css +18 -0
  133. package/src/css/look-board-detail.css +126 -0
  134. package/src/css/look-board-index.css +95 -1
  135. package/src/css/look-board.css +90 -1
  136. package/src/integrations/builtin/contacts/CnContactsTab.vue +101 -26
  137. package/src/integrations/builtin/contacts/__tests__/CnContactsTabBoardRows.spec.js +67 -0
  138. package/src/integrations/builtin/deck/CnDeckCard.vue +121 -2
  139. package/src/integrations/builtin/deck/__tests__/CnDeckCardHeaderActions.spec.js +68 -0
  140. package/src/utils/buildViewTree.js +23 -8
@@ -21,6 +21,22 @@
21
21
  -->
22
22
  <template>
23
23
  <CnDetailCard :title="cardHeaderTitle" :icon="cardIcon" :collapsible="collapsible">
24
+ <!-- `headerActions`: the Deck tab's link and create flows in the title row (detail page). -->
25
+ <template v-if="shownHeaderActions.length > 0" #actions>
26
+ <NcButton
27
+ v-for="action in shownHeaderActions"
28
+ :key="action"
29
+ variant="secondary"
30
+ :data-testid="`cn-deck-card-${action}`"
31
+ @click.stop="action === 'create' ? (createOpen = true) : (pickerOpen = true)">
32
+ <template #icon>
33
+ <Plus v-if="action === 'create'" :size="20" />
34
+ <LinkVariant v-else :size="20" />
35
+ </template>
36
+ {{ action === 'create' ? createLabel : linkLabel }}
37
+ </NcButton>
38
+ </template>
39
+
24
40
  <NcLoadingIcon v-if="loading" />
25
41
 
26
42
  <!-- single-entity surface: chip -->
@@ -102,13 +118,33 @@
102
118
  </section>
103
119
  </div>
104
120
  </template>
121
+
122
+ <p v-if="actionError" class="cn-deck-card__action-error" role="alert">
123
+ {{ actionError }}
124
+ </p>
125
+
126
+ <CnDeckCardPicker
127
+ v-if="pickerOpen"
128
+ :apiBase="apiBase"
129
+ @close="pickerOpen = false"
130
+ @link="onLink" />
131
+
132
+ <CnDeckCardCreate
133
+ v-if="createOpen"
134
+ :apiBase="apiBase"
135
+ @close="createOpen = false"
136
+ @create="onCreate" />
105
137
  </CnDetailCard>
106
138
  </template>
107
139
 
108
140
  <script>
109
141
  import { translatePlural as n, translate as t } from '@nextcloud/l10n'
110
- import { NcLoadingIcon } from '@nextcloud/vue'
142
+ import { NcButton, NcLoadingIcon } from '@nextcloud/vue'
143
+ import LinkVariant from 'vue-material-design-icons/LinkVariant.vue'
144
+ import Plus from 'vue-material-design-icons/Plus.vue'
111
145
  import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'
146
+ import CnDeckCardCreate from '../../../components/CnDeckCardCreate/CnDeckCardCreate.vue'
147
+ import CnDeckCardPicker from '../../../components/CnDeckCardPicker/CnDeckCardPicker.vue'
112
148
  import CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'
113
149
  import { buildHeaders, prefixUrl, safeHref } from '../../../utils/index.js'
114
150
 
@@ -123,7 +159,7 @@ const VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'singl
123
159
  export default {
124
160
  name: 'CnDeckCard',
125
161
 
126
- components: { CnDetailCard, NcLoadingIcon, ViewColumnOutline },
162
+ components: { CnDeckCardCreate, CnDeckCardPicker, CnDetailCard, LinkVariant, NcButton, NcLoadingIcon, Plus, ViewColumnOutline },
127
163
 
128
164
  props: {
129
165
  /** Stable integration id (forwarded from the registry — always `'deck'`). */
@@ -157,6 +193,16 @@ export default {
157
193
  unavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Deck is currently unavailable.') },
158
194
  /** URL of the NC Deck app entry. */
159
195
  deckAppUrl: { type: String, default: '/index.php/apps/deck' },
196
+ /**
197
+ * Buttons in the card's title row on a detail page: `link` links an
198
+ * existing Deck card, `create` makes a new one linked to the object.
199
+ * In the order given; none by default.
200
+ */
201
+ headerActions: { type: Array, default: () => [] },
202
+ /** Label of the `link` button. */
203
+ linkLabel: { type: String, default: () => t('nextcloud-vue', 'Link an existing card') },
204
+ /** Label of the `create` button. */
205
+ createLabel: { type: String, default: () => t('nextcloud-vue', 'New card') },
160
206
  },
161
207
 
162
208
  data() {
@@ -165,10 +211,26 @@ export default {
165
211
  entity: null,
166
212
  loading: false,
167
213
  degraded: '',
214
+ pickerOpen: false,
215
+ createOpen: false,
216
+ actionError: '',
168
217
  }
169
218
  },
170
219
 
171
220
  computed: {
221
+ /**
222
+ * The title-row buttons to draw: only on a detail page with an
223
+ * object, only the known ones, in the order given.
224
+ *
225
+ * @return {Array<string>}
226
+ */
227
+ shownHeaderActions() {
228
+ if (this.surface !== 'detail-page' || !this.objectId || !Array.isArray(this.headerActions)) {
229
+ return []
230
+ }
231
+ return this.headerActions.filter((action, index, all) => ['create', 'link'].includes(action) && all.indexOf(action) === index)
232
+ },
233
+
172
234
  cardHeaderTitle() {
173
235
  return this.title || this.integrationId
174
236
  },
@@ -263,6 +325,58 @@ export default {
263
325
  },
264
326
 
265
327
  methods: {
328
+ /** @return {string} The object's Deck endpoint the link and create flows post to. */
329
+ deckEndpoint() {
330
+ return `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/deck`
331
+ },
332
+
333
+ /**
334
+ * Link the picked card, then read the list again.
335
+ *
336
+ * @param {object} payload `{ cardId }` from the picker.
337
+ */
338
+ async onLink(payload) {
339
+ this.pickerOpen = false
340
+ await this.post(this.deckEndpoint(), payload, t('nextcloud-vue', 'Could not link card.'))
341
+ },
342
+
343
+ /**
344
+ * Create the card linked to the object, then read the list again.
345
+ *
346
+ * @param {object} payload `{ boardId, stackId, title, ... }` from the form.
347
+ */
348
+ async onCreate(payload) {
349
+ this.createOpen = false
350
+ await this.post(`${this.deckEndpoint()}/new`, payload, t('nextcloud-vue', 'Could not create card.'))
351
+ },
352
+
353
+ /**
354
+ * POST one flow's payload and reload on success.
355
+ *
356
+ * @param {string} url The endpoint.
357
+ * @param {object} payload The body.
358
+ * @param {string} failure The sentence shown when it fails.
359
+ */
360
+ async post(url, payload, failure) {
361
+ this.actionError = ''
362
+ try {
363
+ const response = await fetch(prefixUrl(url), {
364
+ method: 'POST',
365
+ headers: { ...buildHeaders(), 'Content-Type': 'application/json' },
366
+ body: JSON.stringify(payload),
367
+ })
368
+ if (response.ok) {
369
+ await this.fetchList()
370
+ } else if (response.status === 409) {
371
+ this.actionError = t('nextcloud-vue', 'This card is already linked.')
372
+ } else {
373
+ this.actionError = failure
374
+ }
375
+ } catch {
376
+ this.actionError = failure
377
+ }
378
+ },
379
+
266
380
  baseUrl() {
267
381
  return `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/integrations/${this.integrationId}`
268
382
  },
@@ -366,6 +480,11 @@ export default {
366
480
  </script>
367
481
 
368
482
  <style scoped>
483
+ .cn-deck-card__action-error {
484
+ margin: 8px 0 0;
485
+ color: var(--color-error-text, var(--color-error));
486
+ }
487
+
369
488
  .cn-deck-card__empty {
370
489
  color: var(--color-text-maxcontrast);
371
490
  font-size: 0.9em;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * CnDeckCard `headerActions` (DqZaakTaken "Kaarten": "Bestaande kaart
3
+ * koppelen" and "Nieuwe kaart" in the title row): on a detail page with an
4
+ * object, the card offers the Deck tab's link and create flows and reloads
5
+ * after either.
6
+ *
7
+ * @spec openspec/changes/deck-card-header-actions/specs/deck-card/spec.md
8
+ */
9
+
10
+ const { flushPromises, mount } = require('@vue/test-utils')
11
+ const CnDeckCard = require('../CnDeckCard.vue').default
12
+
13
+ const PROPS = { register: 'reg', schema: 'case', objectId: 'obj-1', surface: 'detail-page' }
14
+
15
+ function ok(body = { results: [] }) {
16
+ return { ok: true, status: 200, json: () => Promise.resolve(body) }
17
+ }
18
+
19
+ describe('CnDeckCard headerActions', () => {
20
+ beforeEach(() => {
21
+ global.fetch = jest.fn().mockResolvedValue(ok())
22
+ })
23
+
24
+ afterEach(() => {
25
+ delete global.fetch
26
+ })
27
+
28
+ it('draws no buttons by default', async () => {
29
+ const w = mount(CnDeckCard, { props: PROPS })
30
+ await flushPromises()
31
+ expect(w.find('[data-testid="cn-deck-card-link"]').exists()).toBe(false)
32
+ expect(w.find('[data-testid="cn-deck-card-create"]').exists()).toBe(false)
33
+ w.unmount()
34
+ })
35
+
36
+ it('draws link and create in the order given, only on a detail page', async () => {
37
+ const w = mount(CnDeckCard, { props: { ...PROPS, headerActions: ['link', 'create'] } })
38
+ await flushPromises()
39
+ const buttons = w.findAll('[data-testid^="cn-deck-card-"]').filter((b) => ['cn-deck-card-link', 'cn-deck-card-create'].includes(b.attributes('data-testid')))
40
+ expect(buttons.map((b) => b.attributes('data-testid'))).toEqual(['cn-deck-card-link', 'cn-deck-card-create'])
41
+ await w.setProps({ surface: 'user-dashboard' })
42
+ expect(w.find('[data-testid="cn-deck-card-link"]').exists()).toBe(false)
43
+ w.unmount()
44
+ })
45
+
46
+ it('links a picked card through the deck endpoint and reloads', async () => {
47
+ const w = mount(CnDeckCard, { props: { ...PROPS, headerActions: ['link'] } })
48
+ await flushPromises()
49
+ global.fetch.mockClear()
50
+ await w.vm.onLink({ cardId: 7 })
51
+ await flushPromises()
52
+ expect(global.fetch.mock.calls[0][0]).toContain('/objects/reg/case/obj-1/deck')
53
+ expect(global.fetch.mock.calls[0][1].method).toBe('POST')
54
+ expect(global.fetch.mock.calls[1][0]).toContain('/integrations/deck')
55
+ w.unmount()
56
+ })
57
+
58
+ it('creates a card through the deck endpoint and reloads', async () => {
59
+ const w = mount(CnDeckCard, { props: { ...PROPS, headerActions: ['create'] } })
60
+ await flushPromises()
61
+ global.fetch.mockClear()
62
+ await w.vm.onCreate({ boardId: 1, stackId: 2, title: 'Offerte' })
63
+ await flushPromises()
64
+ expect(global.fetch.mock.calls[0][0]).toContain('/objects/reg/case/obj-1/deck/new')
65
+ expect(global.fetch.mock.calls[1][0]).toContain('/integrations/deck')
66
+ w.unmount()
67
+ })
68
+ })
@@ -164,15 +164,15 @@ export function buildViewTree({ views = [], labelFilter = '', maxDepth = 3 } = {
164
164
  }
165
165
  }
166
166
 
167
- // Seeded roots first, then the reader's own, each alphabetically. A
168
- // dropdown whose order changes between two openings is a dropdown nobody
169
- // can build a habit with.
167
+ // Seeded roots first, in the order the app declares them, then the
168
+ // reader's own in the order they were saved (DqFilterBeheren). Both are
169
+ // stable between two openings, so a person can build a habit with them.
170
170
  const seededRoots = roots.filter(({ view }) => view?.seeded === true)
171
171
  const ownRoots = roots.filter(({ view }) => view?.seeded !== true)
172
- for (const { view, orphaned } of sortRootsByName(seededRoots)) {
172
+ for (const { view, orphaned } of seededRoots) {
173
173
  emit(view, 0, orphaned)
174
174
  }
175
- for (const { view, orphaned } of sortRootsByName(ownRoots)) {
175
+ for (const { view, orphaned } of sortRootsBySaved(ownRoots)) {
176
176
  emit(view, 0, orphaned)
177
177
  }
178
178
 
@@ -228,11 +228,26 @@ function sortByName(views) {
228
228
  }
229
229
 
230
230
  /**
231
- * Root entries by the name of the view they carry.
231
+ * Root entries in the order their views were saved (`created`, oldest
232
+ * first); views without a saved time follow, by name.
232
233
  *
233
234
  * @param {Array<object>} roots - The root entries.
234
235
  * @return {Array<object>} The same entries, sorted.
235
236
  */
236
- function sortRootsByName(roots) {
237
- return [...roots].sort((left, right) => String(left.view?.name ?? '').localeCompare(String(right.view?.name ?? '')))
237
+ function sortRootsBySaved(roots) {
238
+ const savedAt = (entry) => (typeof entry.view?.created === 'string' ? entry.view.created : '')
239
+ return [...roots].sort((left, right) => {
240
+ const a = savedAt(left)
241
+ const b = savedAt(right)
242
+ if (a !== b) {
243
+ if (a === '') {
244
+ return 1
245
+ }
246
+ if (b === '') {
247
+ return -1
248
+ }
249
+ return a < b ? -1 : 1
250
+ }
251
+ return String(left.view?.name ?? '').localeCompare(String(right.view?.name ?? ''))
252
+ })
238
253
  }