@conduction/nextcloud-vue 2.74.0 → 2.76.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.
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +19 -14
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +2 -2
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +77 -26
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +22 -14
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +23 -4
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue.js +9 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue.js.map +1 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js +107 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js +79 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -0
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +6 -0
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -2
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnCredentials/CnCredentials.vue2.js +11 -10
- package/dist/esm/components/CnCredentials/CnCredentials.vue2.js.map +1 -1
- package/dist/esm/components/CnCredentials/CnCredentials.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +8 -5
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js +4 -1
- package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js.map +1 -1
- package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +14 -7
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +32 -0
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +6 -3
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +144 -6
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +14 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/selfModeActions.js +90 -14
- package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +15 -3
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +26 -0
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +26 -9
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +19 -3
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +18 -2
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +2 -2
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +153 -38
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +74 -56
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +16 -0
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js +2 -2
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
- package/dist/esm/composables/useObjectLock.js +41 -10
- package/dist/esm/composables/useObjectLock.js.map +1 -1
- package/dist/esm/directives/cnSelectAria.js +55 -0
- package/dist/esm/directives/cnSelectAria.js.map +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +7 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +8 -2
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +154 -2
- package/dist/esm/store/useObjectStore.js +37 -9
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/cnFetch.js +17 -2
- package/dist/esm/utils/cnFetch.js.map +1 -1
- package/dist/esm/utils/objectSignals.js +44 -0
- package/dist/esm/utils/objectSignals.js.map +1 -0
- package/dist/esm/utils/resolveBrand.js +38 -0
- package/dist/esm/utils/resolveBrand.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +9640 -8649
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +154 -2
- package/l10n/en.json +7 -1
- package/l10n/nl.json +8 -2
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +21 -16
- package/src/components/CnAppRoot/CnAppRoot.vue +53 -0
- package/src/components/CnBannerWidget/CnBannerWidget.vue +23 -4
- package/src/components/CnBrandBar/CnBrandBar.vue +230 -0
- package/src/components/CnCardGrid/CnCardGrid.md +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +7 -0
- package/src/components/CnCredentials/CnCredentials.vue +17 -16
- package/src/components/CnDetailPage/CnDetailPage.vue +8 -5
- package/src/components/CnEnvironmentBanner/CnEnvironmentBanner.vue +4 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +10 -0
- package/src/components/CnFormPage/CnFormPage.vue +33 -0
- package/src/components/CnIndexPage/CnIndexPage.md +15 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +157 -18
- package/src/components/CnIndexPage/selfModeActions.js +89 -13
- package/src/components/CnIndexPage/useSelfFetchList.js +15 -3
- package/src/components/CnObjectCard/CnObjectCard.md +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +89 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +19 -3
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +20 -4
- package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +181 -38
- package/src/components/CnStatsBlock/CnStatsBlock.vue +18 -2
- package/src/composables/__tests__/useObjectLock.endpoints.spec.js +83 -0
- package/src/composables/useObjectLock.js +41 -10
- package/src/css/form-field.css +1 -1
- package/src/css/look-board-index.css +35 -1
- package/src/directives/cnSelectAria.js +54 -0
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +6 -1
- package/src/store/useObjectStore.js +37 -9
- package/src/utils/cnFetch.js +16 -1
- package/src/utils/index.js +1 -0
- package/src/utils/objectSignals.js +41 -0
- package/src/utils/resolveBrand.js +37 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
CnBrandBar — the brand block in Nextcloud's own header (screens-brand-block-top-bar).
|
|
3
|
+
|
|
4
|
+
Draws the 237px brand block (emblem, organisation, app name, and a 1px
|
|
5
|
+
divider at its end) at the START of `#header`, before the app menu, under the
|
|
6
|
+
board look. CnAppRoot mounts it when a brand is declared and `#header` exists.
|
|
7
|
+
|
|
8
|
+
`#header` is Nextcloud's and offers no extension point for apps, so this is
|
|
9
|
+
the one place the library reaches into it, and it does so defensively:
|
|
10
|
+
- it adds ONE node of its own (a host element) as the first child of the
|
|
11
|
+
header, and teleports the block into it; it never moves, edits or removes
|
|
12
|
+
a node Nextcloud drew;
|
|
13
|
+
- it re-inserts its own host when Nextcloud re-renders the header (a
|
|
14
|
+
MutationObserver on the header and its parent), never a second one;
|
|
15
|
+
- with no `#header` it renders nothing and emits `unavailable`, so
|
|
16
|
+
CnAppRoot lets the navigation draw the block instead.
|
|
17
|
+
|
|
18
|
+
Sizes come from the board: `dossiq/DqKop`.
|
|
19
|
+
-->
|
|
20
|
+
<template>
|
|
21
|
+
<Teleport v-if="host" :to="host">
|
|
22
|
+
<div class="cn-brand-bar" data-testid="cn-brand-bar">
|
|
23
|
+
<div class="cn-brand-bar__brand" data-testid="cn-brand-bar-brand">
|
|
24
|
+
<!-- Decorative beside a name: the name already says whose app this is. -->
|
|
25
|
+
<img
|
|
26
|
+
v-if="typeof brand.emblem === 'string' && brand.emblem !== ''"
|
|
27
|
+
class="cn-brand-bar__emblem"
|
|
28
|
+
data-testid="cn-brand-bar-emblem"
|
|
29
|
+
:src="brand.emblem"
|
|
30
|
+
:alt="brand.name ? '' : brand.alt">
|
|
31
|
+
<span
|
|
32
|
+
v-else-if="brand.emblem === true"
|
|
33
|
+
class="cn-brand-bar__emblem cn-brand-bar__emblem--theme"
|
|
34
|
+
data-testid="cn-brand-bar-emblem"
|
|
35
|
+
aria-hidden="true" />
|
|
36
|
+
<img
|
|
37
|
+
v-else-if="brand.logo"
|
|
38
|
+
class="cn-brand-bar__emblem"
|
|
39
|
+
:src="brand.logo"
|
|
40
|
+
:alt="brand.name ? '' : brand.alt">
|
|
41
|
+
<span v-if="brand.name || brand.caption" class="cn-brand-bar__text">
|
|
42
|
+
<span v-if="brand.caption" class="cn-brand-bar__caption">{{ brand.caption }}</span>
|
|
43
|
+
<strong v-if="brand.name" class="cn-brand-bar__name">{{ brand.name }}</strong>
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
<span class="cn-brand-bar__divider" aria-hidden="true" />
|
|
47
|
+
</div>
|
|
48
|
+
</Teleport>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script>
|
|
52
|
+
import { markRaw } from 'vue'
|
|
53
|
+
|
|
54
|
+
const HOST_CLASS = 'cn-brand-bar-host'
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The brand block in Nextcloud's header.
|
|
58
|
+
*
|
|
59
|
+
* @event unavailable Emitted when there is no `#header` to put the block in (or it disappears), so the host lets the navigation draw it.
|
|
60
|
+
*
|
|
61
|
+
* @spec openspec/changes/screens-brand-block-top-bar/specs/layout-components/spec.md#requirement-the-board-look-draws-the-brand-block-in-the-nextcloud-header
|
|
62
|
+
*/
|
|
63
|
+
export default {
|
|
64
|
+
name: 'CnBrandBar',
|
|
65
|
+
|
|
66
|
+
props: {
|
|
67
|
+
/**
|
|
68
|
+
* The resolved brand: `{ logo, emblem, name, caption, alt }`. `emblem`
|
|
69
|
+
* is a URL, or `true` for the theme's `--nldesign-emblem-url`.
|
|
70
|
+
*
|
|
71
|
+
* @type {{logo?: string, emblem?: (string|boolean), name?: string, caption?: string, alt?: string}}
|
|
72
|
+
*/
|
|
73
|
+
brand: {
|
|
74
|
+
type: Object,
|
|
75
|
+
required: true,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
emits: ['unavailable'],
|
|
80
|
+
|
|
81
|
+
data() {
|
|
82
|
+
return { host: null, observer: null }
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
mounted() {
|
|
86
|
+
this.attach()
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
beforeUnmount() {
|
|
90
|
+
this.detach()
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
methods: {
|
|
94
|
+
/**
|
|
95
|
+
* Insert the host as the first child of `#header` and watch the header.
|
|
96
|
+
* Emits `unavailable` when there is no header.
|
|
97
|
+
*
|
|
98
|
+
* @return {void}
|
|
99
|
+
*/
|
|
100
|
+
attach() {
|
|
101
|
+
const header = document.getElementById('header')
|
|
102
|
+
if (!header) {
|
|
103
|
+
/**
|
|
104
|
+
* @event unavailable Emitted when there is no `#header` to put the block in.
|
|
105
|
+
*/
|
|
106
|
+
this.$emit('unavailable')
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
this.host = markRaw(document.createElement('div'))
|
|
110
|
+
this.host.className = HOST_CLASS
|
|
111
|
+
header.insertBefore(this.host, header.firstChild)
|
|
112
|
+
if (typeof MutationObserver === 'function') {
|
|
113
|
+
this.observer = new MutationObserver(() => this.reattach())
|
|
114
|
+
this.observer.observe(header, { childList: true })
|
|
115
|
+
if (header.parentNode) {
|
|
116
|
+
this.observer.observe(header.parentNode, { childList: true })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Put the same host back when Nextcloud re-rendered the header and took
|
|
123
|
+
* it out. Never creates a second host; when the header is gone for good
|
|
124
|
+
* it hands the block back to the navigation.
|
|
125
|
+
*
|
|
126
|
+
* @return {void}
|
|
127
|
+
*/
|
|
128
|
+
reattach() {
|
|
129
|
+
if (!this.host) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
const header = document.getElementById('header')
|
|
133
|
+
if (!header) {
|
|
134
|
+
this.$emit('unavailable')
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if (this.host.parentNode !== header) {
|
|
138
|
+
header.insertBefore(this.host, header.firstChild)
|
|
139
|
+
this.observer?.observe(header, { childList: true })
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Stop watching and remove only the host this component added.
|
|
145
|
+
*
|
|
146
|
+
* @return {void}
|
|
147
|
+
*/
|
|
148
|
+
detach() {
|
|
149
|
+
this.observer?.disconnect()
|
|
150
|
+
this.observer = null
|
|
151
|
+
this.host?.remove()
|
|
152
|
+
this.host = null
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
</script>
|
|
157
|
+
|
|
158
|
+
<style>
|
|
159
|
+
/* The host is one flex item at the start of Nextcloud's header. */
|
|
160
|
+
.cn-brand-bar-host {
|
|
161
|
+
flex: none;
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
height: 100%;
|
|
165
|
+
margin-inline-end: 12px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.cn-brand-bar {
|
|
169
|
+
box-sizing: border-box;
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: 12px;
|
|
173
|
+
width: var(--cn-board-brand-width, 237px);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.cn-brand-bar__brand {
|
|
177
|
+
flex: 1 1 0;
|
|
178
|
+
box-sizing: border-box;
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 10px;
|
|
182
|
+
min-width: 0;
|
|
183
|
+
min-height: 48px;
|
|
184
|
+
max-height: 100%;
|
|
185
|
+
padding: 0 6px;
|
|
186
|
+
border-radius: 8px;
|
|
187
|
+
color: var(--color-background-plain-text, var(--color-main-text));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.cn-brand-bar__emblem {
|
|
191
|
+
flex: none;
|
|
192
|
+
height: var(--cn-nav-emblem-size, 34px);
|
|
193
|
+
width: auto;
|
|
194
|
+
object-fit: contain;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.cn-brand-bar__emblem--theme {
|
|
198
|
+
display: inline-block;
|
|
199
|
+
width: var(--cn-nav-emblem-size, 34px);
|
|
200
|
+
background: var(--nldesign-emblem-url) center / contain no-repeat;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.cn-brand-bar__text {
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-direction: column;
|
|
206
|
+
line-height: 1.15;
|
|
207
|
+
min-width: 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.cn-brand-bar__caption {
|
|
211
|
+
font-size: 13px;
|
|
212
|
+
white-space: nowrap;
|
|
213
|
+
opacity: 0.8;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.cn-brand-bar__name {
|
|
217
|
+
font-size: 18px;
|
|
218
|
+
font-weight: 700;
|
|
219
|
+
white-space: nowrap;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.cn-brand-bar__divider {
|
|
223
|
+
flex: none;
|
|
224
|
+
width: 1px;
|
|
225
|
+
height: 28px;
|
|
226
|
+
background: currentColor;
|
|
227
|
+
opacity: 0.25;
|
|
228
|
+
color: var(--color-background-plain-text, var(--color-main-text));
|
|
229
|
+
}
|
|
230
|
+
</style>
|
|
@@ -106,6 +106,7 @@ Custom card slot — replace the default CnObjectCard with your own rendering:
|
|
|
106
106
|
|---|---|---|---|
|
|
107
107
|
| `selectable` | `Boolean` | `false` | When `true`, each card shows a selection checkbox and the grid emits `select` events |
|
|
108
108
|
| `selectedIds` | `Array` | `[]` | Array of currently selected object IDs (matched against `object[rowKey]`) |
|
|
109
|
+
| `accentOf` | `Function` | `null` | `(object) => { variant, icon?, label? } \| null`, each card's status accent (see CnObjectCard `accent`) |
|
|
109
110
|
|
|
110
111
|
## Board look props
|
|
111
112
|
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
:status="statusOf ? statusOf(object) : null"
|
|
41
41
|
:leading="leadingOf ? leadingOf(object) : null"
|
|
42
42
|
:footerAction="footerActionOf ? footerActionOf(object) : null"
|
|
43
|
+
:accent="accentOf ? accentOf(object) : null"
|
|
43
44
|
v-on="cardListeners(object)">
|
|
44
45
|
<template v-if="$slots['card-actions']" #actions="{ object: obj }">
|
|
45
46
|
<slot name="card-actions" :object="obj" />
|
|
@@ -182,6 +183,12 @@ export default {
|
|
|
182
183
|
default: null,
|
|
183
184
|
},
|
|
184
185
|
|
|
186
|
+
/** `(object) => { variant, icon?, label? }` for each card's status accent. See CnObjectCard `accent`. */
|
|
187
|
+
accentOf: {
|
|
188
|
+
type: Function,
|
|
189
|
+
default: null,
|
|
190
|
+
},
|
|
191
|
+
|
|
185
192
|
/** Text shown when there are no objects */
|
|
186
193
|
emptyText: {
|
|
187
194
|
type: String,
|
|
@@ -2,25 +2,25 @@
|
|
|
2
2
|
- SPDX-License-Identifier: EUPL-1.2
|
|
3
3
|
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
4
4
|
-
|
|
5
|
-
- CnCredentials
|
|
5
|
+
- CnCredentials: the settings surface for the OpenRegister credential
|
|
6
6
|
- broker. Apps occasionally need to act on your behalf against an external
|
|
7
7
|
- service (GitHub, GitLab, …). Rather than hand the secret to the app, you
|
|
8
|
-
- give it once to OpenRegister, which stores it in Keepiq
|
|
8
|
+
- give it once to OpenRegister, which stores it in Keepiq, Nextcloud's
|
|
9
9
|
- native, encrypted credential vault. Apps then make the outbound call
|
|
10
10
|
- THROUGH OpenRegister and never see the secret itself. You decide which
|
|
11
11
|
- apps may use each credential, so one credential can be shared across apps
|
|
12
12
|
- or kept dedicated to one.
|
|
13
13
|
-
|
|
14
14
|
- Two scopes render from this one component:
|
|
15
|
-
- • scope="personal" (default)
|
|
15
|
+
- • scope="personal" (default), the signed-in user's own
|
|
16
16
|
- credentials, shown in an app's *personal* settings. From here you may
|
|
17
17
|
- only add/remove *the app you are currently in* to/from a credential,
|
|
18
18
|
- or add a new credential (the current app is auto-added). You never
|
|
19
19
|
- manage the full app list of another app from here.
|
|
20
|
-
- • scope="organisation"
|
|
20
|
+
- • scope="organisation" , organisation-wide credentials, shown in an
|
|
21
21
|
- app's *admin* settings. An admin manages the full allowed-app list.
|
|
22
22
|
-
|
|
23
|
-
- Talks to OpenRegister's credential endpoints (metadata only
|
|
23
|
+
- Talks to OpenRegister's credential endpoints (metadata only, a secret is
|
|
24
24
|
- write-only and is NEVER returned or displayed):
|
|
25
25
|
- GET /apps/openregister/api/credentials?scope={scope}
|
|
26
26
|
- GET /apps/openregister/api/credentials/providers
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
</ul>
|
|
149
149
|
</section>
|
|
150
150
|
|
|
151
|
-
<!-- Add credential
|
|
151
|
+
<!-- Add credential: inline wizard: pick a provider, then fill it in. -->
|
|
152
152
|
<section class="cn-credentials__section">
|
|
153
153
|
<NcButton v-if="!adding"
|
|
154
154
|
variant="secondary"
|
|
@@ -269,7 +269,7 @@ const PROVIDERS_PATH = '/apps/openregister/api/credentials/providers'
|
|
|
269
269
|
* Hardcoded per-provider presentation. Keyed by the OpenRegister catalogue
|
|
270
270
|
* identifier. `colour` drives the tile; `setupHelp`/`setupUrl` explain how to
|
|
271
271
|
* obtain the secret; `secretLabel` names the field. Providers not listed here
|
|
272
|
-
* still work
|
|
272
|
+
* still work, they render a neutral tile from their identifier.
|
|
273
273
|
*/
|
|
274
274
|
const PROVIDER_META = {
|
|
275
275
|
github: {
|
|
@@ -339,7 +339,7 @@ export default {
|
|
|
339
339
|
},
|
|
340
340
|
|
|
341
341
|
/**
|
|
342
|
-
* The current app's manifest `credentials[]` declarations
|
|
342
|
+
* The current app's manifest `credentials[]` declarations: the
|
|
343
343
|
* providers this app can reach through the broker. Shape:
|
|
344
344
|
* `[{ provider, reason, scopes }]`. Rendered read-only under the intro.
|
|
345
345
|
*
|
|
@@ -404,15 +404,16 @@ export default {
|
|
|
404
404
|
},
|
|
405
405
|
|
|
406
406
|
/**
|
|
407
|
-
* Reframed intro copy per scope.
|
|
407
|
+
* Reframed intro copy per scope. No em-dashes, sentences under 16 words.
|
|
408
408
|
*
|
|
409
|
+
* @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/credentials-settings/spec.md
|
|
409
410
|
* @return {string} Intro text.
|
|
410
411
|
*/
|
|
411
412
|
introText() {
|
|
412
413
|
if (this.scope === 'organisation') {
|
|
413
|
-
return t('nextcloud-vue', 'Organisation credentials let apps act
|
|
414
|
+
return t('nextcloud-vue', 'Organisation credentials let apps act for your organisation. Keepiq, the credential vault of Nextcloud, keeps the secret. The app never holds it. You choose which apps may use each credential.')
|
|
414
415
|
}
|
|
415
|
-
return t('nextcloud-vue', 'Apps sometimes
|
|
416
|
+
return t('nextcloud-vue', 'Apps sometimes act for you on an external service. You give the secret to Nextcloud once, and Keepiq keeps it in an encrypted vault. Apps make the call through Keepiq and never see the secret. You choose which apps may use each credential. Share one across apps, or keep one per app.')
|
|
416
417
|
},
|
|
417
418
|
|
|
418
419
|
/**
|
|
@@ -438,7 +439,7 @@ export default {
|
|
|
438
439
|
const serverIds = this.providers.map((p) => p.identifier)
|
|
439
440
|
let ids = serverIds.length ? serverIds : Object.keys(PROVIDER_META)
|
|
440
441
|
// When the app declares which providers it uses (appCredentials),
|
|
441
|
-
// only offer those
|
|
442
|
+
// only offer those, an app should not let you add a credential for
|
|
442
443
|
// a provider it has no code path to use.
|
|
443
444
|
if (this.supportedProviders.size > 0) {
|
|
444
445
|
ids = ids.filter((id) => this.supportedProviders.has(id))
|
|
@@ -448,7 +449,7 @@ export default {
|
|
|
448
449
|
|
|
449
450
|
/**
|
|
450
451
|
* Provider identifiers the app declares it uses, derived from
|
|
451
|
-
* `appCredentials`. Empty when the app declares nothing
|
|
452
|
+
* `appCredentials`. Empty when the app declares nothing, filtering is
|
|
452
453
|
* then a no-op, so consumers that don't declare providers are unchanged.
|
|
453
454
|
*
|
|
454
455
|
* @return {Set<string>} Supported provider identifiers.
|
|
@@ -460,7 +461,7 @@ export default {
|
|
|
460
461
|
},
|
|
461
462
|
|
|
462
463
|
/**
|
|
463
|
-
* Stored credentials to display
|
|
464
|
+
* Stored credentials to display: filtered to the app's supported
|
|
464
465
|
* providers when it declares any, so you can only see and authorise
|
|
465
466
|
* credentials the app can actually use; otherwise all of them.
|
|
466
467
|
*
|
|
@@ -503,7 +504,7 @@ export default {
|
|
|
503
504
|
},
|
|
504
505
|
|
|
505
506
|
/**
|
|
506
|
-
* Whether the add form can be submitted
|
|
507
|
+
* Whether the add form can be submitted: name, provider and a secret
|
|
507
508
|
* are required (the whole point is to store a secret).
|
|
508
509
|
*
|
|
509
510
|
* @return {boolean} True when submittable.
|
|
@@ -727,7 +728,7 @@ export default {
|
|
|
727
728
|
},
|
|
728
729
|
|
|
729
730
|
/**
|
|
730
|
-
* Human-readable provider title
|
|
731
|
+
* Human-readable provider title: hardcoded catalogue first, then the
|
|
731
732
|
* server title, then the raw identifier.
|
|
732
733
|
*
|
|
733
734
|
* @param {string} identifier The provider identifier.
|
|
@@ -2727,18 +2727,21 @@ export default {
|
|
|
2727
2727
|
() => props.objectId,
|
|
2728
2728
|
{ enabled: () => Boolean(resolveType() && props.objectId) },
|
|
2729
2729
|
)
|
|
2730
|
-
|
|
2731
|
-
|
|
2730
|
+
// Getters, read at call time: a register or schema that arrives after
|
|
2731
|
+
// mount (route params, a resolved sidebar) must reach the lock URL. They
|
|
2732
|
+
// used to be consts captured once at setup.
|
|
2733
|
+
const sidebarReg = () => props.sidebarProps?.register || props.resolvedSidebar?.register || props.register || ''
|
|
2734
|
+
const sidebarSchema = () => props.sidebarProps?.schema || props.resolvedSidebar?.schema || props.schema || ''
|
|
2732
2735
|
const lock = useObjectLock(
|
|
2733
2736
|
subscriptionStore,
|
|
2734
|
-
|
|
2735
|
-
() => resolveType() || sidebarSchema,
|
|
2737
|
+
sidebarReg,
|
|
2738
|
+
() => resolveType() || sidebarSchema(),
|
|
2736
2739
|
() => props.objectId,
|
|
2737
2740
|
// The third argument is the OBJECT CACHE KEY (`<register>-<schema>`)
|
|
2738
2741
|
// and the lock URL needs the schema SLUG. They were one string, and
|
|
2739
2742
|
// the acquire went to `/api/objects/<register>/<register>-<schema>/`,
|
|
2740
2743
|
// which no register answers.
|
|
2741
|
-
{ schemaSlug: () => props.schema || sidebarSchema },
|
|
2744
|
+
{ schemaSlug: () => props.schema || sidebarSchema() },
|
|
2742
2745
|
)
|
|
2743
2746
|
return {
|
|
2744
2747
|
...registryExposed,
|
|
@@ -103,7 +103,10 @@ export default {
|
|
|
103
103
|
return
|
|
104
104
|
}
|
|
105
105
|
const all = Object.values(ENVIRONMENT_TITLE_PREFIX)
|
|
106
|
-
|
|
106
|
+
// Plain string comparison: no pattern is built from the prefixes, so
|
|
107
|
+
// there is nothing to escape.
|
|
108
|
+
const owned = all.find((p) => document.title.startsWith(`${p} `))
|
|
109
|
+
const bare = owned ? document.title.slice(owned.length + 1) : document.title
|
|
107
110
|
const wanted = environment ? `${ENVIRONMENT_TITLE_PREFIX[environment]} ${bare}` : bare
|
|
108
111
|
if (document.title !== wanted) {
|
|
109
112
|
document.title = wanted
|
|
@@ -309,6 +309,7 @@
|
|
|
309
309
|
and select machinery is unchanged either way. -->
|
|
310
310
|
<component
|
|
311
311
|
:is="isUserField(field) ? 'NcSelectUsers' : 'NcSelect'"
|
|
312
|
+
v-cn-select-aria="fieldAria(field)"
|
|
312
313
|
:inputId="'cn-form-' + field.key"
|
|
313
314
|
:inputLabel="fieldLabelText(field)"
|
|
314
315
|
:labelOutside="isBoardLook"
|
|
@@ -358,6 +359,7 @@
|
|
|
358
359
|
<!-- Multiselect (array enum items / $ref array / Nextcloud users, supports async function) -->
|
|
359
360
|
<div v-else-if="field.widget === 'multiselect' || field.widget === 'user-multiselect' || field.widget === 'group-multiselect'" class="cn-form-dialog__select-wrapper">
|
|
360
361
|
<NcSelect
|
|
362
|
+
v-cn-select-aria="fieldAria(field)"
|
|
361
363
|
:inputId="'cn-form-' + field.key"
|
|
362
364
|
:inputLabel="fieldLabelText(field)"
|
|
363
365
|
:labelOutside="isBoardLook"
|
|
@@ -401,6 +403,7 @@
|
|
|
401
403
|
<div v-else-if="field.widget === 'tags'" class="cn-form-dialog__select-wrapper">
|
|
402
404
|
<!-- TODO: restore `:options` to `asyncState[field.key]?.options` once on Vue 3 (buble doesn't support optional chaining) -->
|
|
403
405
|
<NcSelect
|
|
406
|
+
v-cn-select-aria="fieldAria(field)"
|
|
404
407
|
:inputId="'cn-form-' + field.key"
|
|
405
408
|
:inputLabel="fieldLabelText(field)"
|
|
406
409
|
:labelOutside="isBoardLook"
|
|
@@ -789,6 +792,7 @@ import { heldFromFailures, splitHeldFiles, uploadHeldFiles } from '../../composa
|
|
|
789
792
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
790
793
|
import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js'
|
|
791
794
|
import { useWriteFeedback } from '../../composables/useWriteFeedback.js'
|
|
795
|
+
import { cnSelectAria } from '../../directives/cnSelectAria.js'
|
|
792
796
|
import { dialogBoardMixin } from '../../mixins/dialogBoard.js'
|
|
793
797
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
794
798
|
import { resolveCreateOverrideHandler } from '../../utils/actionsDispatcher.js'
|
|
@@ -1009,6 +1013,8 @@ const SEMANTIC_RESOLVE_ENDPOINT = '/apps/openregister/api/schemas/resolve-by-imp
|
|
|
1009
1013
|
export default {
|
|
1010
1014
|
name: 'CnFormDialog',
|
|
1011
1015
|
|
|
1016
|
+
directives: { cnSelectAria },
|
|
1017
|
+
|
|
1012
1018
|
components: {
|
|
1013
1019
|
CnFileField,
|
|
1014
1020
|
CnDurationField,
|
|
@@ -3618,6 +3624,10 @@ export default {
|
|
|
3618
3624
|
return
|
|
3619
3625
|
}
|
|
3620
3626
|
const [head, ...rest] = key.split('.')
|
|
3627
|
+
const unsafe = ['__proto__', 'constructor', 'prototype']
|
|
3628
|
+
if (unsafe.includes(head) || rest.some((k) => unsafe.includes(k))) {
|
|
3629
|
+
return
|
|
3630
|
+
}
|
|
3621
3631
|
const clone = JSON.parse(JSON.stringify(this.formData[head] ?? {}))
|
|
3622
3632
|
let cur = clone
|
|
3623
3633
|
rest.slice(0, -1).forEach((k) => {
|
|
@@ -217,6 +217,7 @@
|
|
|
217
217
|
<component
|
|
218
218
|
:is="resolveFieldRender(field).tag"
|
|
219
219
|
v-if="resolveFieldRender(field)"
|
|
220
|
+
v-cn-select-aria="selectAria(field)"
|
|
220
221
|
v-bind="fieldProps(field)"
|
|
221
222
|
v-on="resolveFieldRender(field).listeners">
|
|
222
223
|
<!-- NcCheckboxRadioSwitch puts its label in the slot -->
|
|
@@ -398,6 +399,7 @@ import CnStepper from '../CnStepper/CnStepper.vue'
|
|
|
398
399
|
import { cnRenderFormField } from '../../composables/cnFormFieldRenderer.js'
|
|
399
400
|
import { draftIndicatorText, draftKey, formDraftMixin, readDraft } from '../../composables/useFormDraft.js'
|
|
400
401
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
402
|
+
import { cnSelectAria } from '../../directives/cnSelectAria.js'
|
|
401
403
|
import { loadCurrentUserProfile } from '../../utils/currentUserProfile.js'
|
|
402
404
|
import { computeAssignments, resolveFieldDefaults } from '../../utils/formAssign.js'
|
|
403
405
|
import { validateFieldValue } from '../../utils/formValidation.js'
|
|
@@ -441,6 +443,8 @@ function resolveParams(url, params) {
|
|
|
441
443
|
export default {
|
|
442
444
|
name: 'CnFormPage',
|
|
443
445
|
|
|
446
|
+
directives: { cnSelectAria },
|
|
447
|
+
|
|
444
448
|
components: {
|
|
445
449
|
CnFormErrorSummary,
|
|
446
450
|
CnFormField,
|
|
@@ -1308,6 +1312,35 @@ export default {
|
|
|
1308
1312
|
return props
|
|
1309
1313
|
},
|
|
1310
1314
|
|
|
1315
|
+
/**
|
|
1316
|
+
* The accessibility attributes of an enum field's NcSelect. NcSelect
|
|
1317
|
+
* forwards no attributes to its input, so these go through
|
|
1318
|
+
* `v-cn-select-aria` onto the combobox input instead of `fieldProps`.
|
|
1319
|
+
* The error element exists for an enum in both looks (NcSelect has no
|
|
1320
|
+
* native error text), so an invalid value lists it before the hint.
|
|
1321
|
+
*
|
|
1322
|
+
* @param {object} field The formField shape.
|
|
1323
|
+
* @return {object|null} The attributes, or null for any other control.
|
|
1324
|
+
*/
|
|
1325
|
+
selectAria(field) {
|
|
1326
|
+
const render = this.resolveFieldRender(field)
|
|
1327
|
+
if (!render || render.kind !== 'enum') {
|
|
1328
|
+
return null
|
|
1329
|
+
}
|
|
1330
|
+
const ids = []
|
|
1331
|
+
if (this.fieldErrors[field.key]) {
|
|
1332
|
+
ids.push(this.errorIdFor(field))
|
|
1333
|
+
}
|
|
1334
|
+
if (field.help) {
|
|
1335
|
+
ids.push(this.helpIdFor(field))
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
'aria-invalid': this.fieldErrors[field.key] ? 'true' : null,
|
|
1339
|
+
'aria-describedby': ids.length > 0 ? ids.join(' ') : null,
|
|
1340
|
+
'aria-required': this.showsBoardHead(field) && this.isFieldRequired(field) ? 'true' : null,
|
|
1341
|
+
}
|
|
1342
|
+
},
|
|
1343
|
+
|
|
1311
1344
|
/**
|
|
1312
1345
|
* Cancel (board look footer): go to `cancelRoute` when set, and tell the
|
|
1313
1346
|
* host.
|
|
@@ -434,6 +434,21 @@ list nested under a parent route (`/forms/:id/submissions`) is a fully
|
|
|
434
434
|
declarative page. When `objects` **is** supplied (every existing consumer),
|
|
435
435
|
nothing changes — no store is touched and `filter` has no effect.
|
|
436
436
|
|
|
437
|
+
### Mixed-schema lists (`collectionUrl`)
|
|
438
|
+
|
|
439
|
+
Set `collectionUrl` to list from an endpoint that searches several
|
|
440
|
+
register/schema pairs at once. `register` and `schema` stay the page's own
|
|
441
|
+
pair (columns, Add, export, import); a row of another pair edits, saves and
|
|
442
|
+
deletes through its own `@self.register` / `@self.schema`.
|
|
443
|
+
|
|
444
|
+
```js static
|
|
445
|
+
<CnIndexPage
|
|
446
|
+
title="Publications"
|
|
447
|
+
register="19"
|
|
448
|
+
schema="24"
|
|
449
|
+
:collection-url="generateUrl('/apps/opencatalogi/api/{slug}', { slug })" />
|
|
450
|
+
```
|
|
451
|
+
|
|
437
452
|
## Named entity sources (`config.entitySource`) — lists that are not OpenRegister objects
|
|
438
453
|
|
|
439
454
|
Self-fetch needs a `register` + `schema` pair. Some lists have neither: a flow
|