@conduction/nextcloud-vue 2.100.0 → 2.101.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/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +22 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +26 -16
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +51 -7
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +104 -73
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js +9 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js +139 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js +76 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +160 -2
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +206 -94
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesReview.js +61 -0
- package/dist/esm/components/CnFilesBrowser/filesReview.js.map +1 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +9 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +279 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +122 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnGroupedCardsWidget/index.js +29 -0
- package/dist/esm/components/CnGroupedCardsWidget/index.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +251 -71
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +317 -209
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js +4 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +15 -0
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +14 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +14 -6
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +10 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +6 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +91 -4
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +63 -38
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +9 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js +175 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js +77 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +35 -2
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +4 -0
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +167 -20
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useEndpointSource.js +28 -6
- package/dist/esm/composables/useEndpointSource.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +37 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +35 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +769 -251
- package/dist/esm/utils/languageMapText.js +50 -0
- package/dist/esm/utils/languageMapText.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +3 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/mapList.js +71 -0
- package/dist/esm/utils/mapList.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +8977 -6843
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +769 -251
- package/l10n/en.json +37 -1
- package/l10n/nl.json +35 -1
- package/package.json +1 -1
- package/src/components/CnChoiceCards/CnChoiceCards.md +39 -0
- package/src/components/CnChoiceCards/CnChoiceCards.vue +93 -16
- package/src/components/CnDashboardPage/CnDashboardPage.vue +89 -20
- package/src/components/CnExportsWidget/CnExportsWidget.vue +233 -0
- package/src/components/CnExportsWidget/index.js +7 -0
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +324 -0
- package/src/components/CnFilesBrowser/filesReview.js +57 -0
- package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +433 -0
- package/src/components/CnGroupedCardsWidget/index.js +28 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +240 -4
- package/src/components/CnIndexPage/useNamedSource.js +4 -1
- package/src/components/CnMapWidget/CnMapWidget.vue +37 -0
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +29 -1
- package/src/components/CnObjectSidebar/CnFilesTab.vue +15 -0
- package/src/components/CnOrderedListField/CnOrderedListField.vue +151 -5
- package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +272 -0
- package/src/components/CnSavedViewAlertWidget/index.js +7 -0
- package/src/components/CnSetupWizard/CnSetupWizard.vue +35 -2
- package/src/components/CnWidgetGrid/builtInWidgets.js +4 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/index.js +3 -0
- package/src/composables/indexSources.js +166 -19
- package/src/composables/useEndpointSource.js +28 -6
- package/src/css/index-page.css +117 -0
- package/src/css/look-board.css +44 -0
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +31 -1
- package/src/utils/languageMapText.js +46 -0
- package/src/utils/libraryWidgetKeys.js +3 -0
- package/src/utils/mapList.js +67 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<div class="cn-exports-widget" data-testid="cn-exports-widget">
|
|
7
|
+
<p v-if="loading && runs.length === 0" class="cn-exports-widget__muted">
|
|
8
|
+
{{ t('nextcloud-vue', 'Loading …') }}
|
|
9
|
+
</p>
|
|
10
|
+
<p v-else-if="failed" class="cn-exports-widget__muted" data-testid="cn-exports-error">
|
|
11
|
+
{{ t('nextcloud-vue', 'Your exports could not be loaded.') }}
|
|
12
|
+
</p>
|
|
13
|
+
<p v-else-if="runs.length === 0" class="cn-exports-widget__muted" data-testid="cn-exports-empty">
|
|
14
|
+
{{ emptyText }}
|
|
15
|
+
</p>
|
|
16
|
+
<ul v-else class="cn-exports-widget__list">
|
|
17
|
+
<li v-for="run in shownRuns"
|
|
18
|
+
:key="run.key"
|
|
19
|
+
class="cn-exports-widget__row"
|
|
20
|
+
data-testid="cn-exports-row">
|
|
21
|
+
<span class="cn-exports-widget__text">
|
|
22
|
+
<span class="cn-exports-widget__name">{{ run.filename }}</span>
|
|
23
|
+
<span class="cn-exports-widget__meta">{{ run.meta }}</span>
|
|
24
|
+
</span>
|
|
25
|
+
<span v-if="run.expiresToday" class="cn-exports-widget__tag" data-testid="cn-exports-expires-today">
|
|
26
|
+
{{ t('nextcloud-vue', 'Expires today') }}
|
|
27
|
+
</span>
|
|
28
|
+
<NcButton v-else-if="run.href"
|
|
29
|
+
:href="run.href"
|
|
30
|
+
data-testid="cn-exports-download">
|
|
31
|
+
{{ t('nextcloud-vue', 'Download') }}
|
|
32
|
+
</NcButton>
|
|
33
|
+
</li>
|
|
34
|
+
</ul>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script>
|
|
39
|
+
import { getCurrentUser } from '@nextcloud/auth'
|
|
40
|
+
import axios from '@nextcloud/axios'
|
|
41
|
+
import { translatePlural as n, translate as t } from '@nextcloud/l10n'
|
|
42
|
+
import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
|
|
43
|
+
import { NcButton } from '@nextcloud/vue'
|
|
44
|
+
import { formatBoardDate } from '../../utils/boardDate.js'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* CnExportsWidget — the reader's own exports (DqZoekresultaten "Uw exports"):
|
|
48
|
+
* each file with when it was made, how often it was downloaded and when it
|
|
49
|
+
* expires, a Download button while the file is there, and "Expires today" on
|
|
50
|
+
* the day it goes.
|
|
51
|
+
*
|
|
52
|
+
* Reads OpenRegister's export runs (`GET /apps/openregister/api/exports`)
|
|
53
|
+
* with the reader as the actor, so an administrator sees their own here too.
|
|
54
|
+
* `content.register` and `content.schema` narrow it to one list's exports.
|
|
55
|
+
* A run whose file was swept, or that never produced one, is left out.
|
|
56
|
+
*
|
|
57
|
+
* Example (manifest, in an index page's `sidePanel`):
|
|
58
|
+
* ```json
|
|
59
|
+
* { "id": "exports", "type": "exports", "title": "Your exports", "content": { "register": "dossiq", "schema": "case", "limit": 5 } }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export default {
|
|
63
|
+
name: 'CnExportsWidget',
|
|
64
|
+
|
|
65
|
+
components: { NcButton },
|
|
66
|
+
|
|
67
|
+
inject: {
|
|
68
|
+
/** Host translate function provided by CnAppRoot. */
|
|
69
|
+
cnTranslate: { default: () => (key) => key },
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
props: {
|
|
73
|
+
/** `{ register?, schema?, limit?, emptyText? }`; `limit` defaults to 5. */
|
|
74
|
+
content: {
|
|
75
|
+
type: Object,
|
|
76
|
+
default: () => ({}),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
data() {
|
|
81
|
+
return { loading: false, failed: false, runs: [], now: new Date() }
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
computed: {
|
|
85
|
+
/** @return {number} How many exports to show. */
|
|
86
|
+
limit() {
|
|
87
|
+
const limit = Number(this.content && this.content.limit)
|
|
88
|
+
return Number.isInteger(limit) && limit > 0 ? limit : 5
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/** @return {Array<object>} The exports to draw, newest first. */
|
|
92
|
+
shownRuns() {
|
|
93
|
+
return this.runs
|
|
94
|
+
.filter((run) => run && run.filename && run.fileId !== null && run.fileId !== undefined && run.status !== 'expired')
|
|
95
|
+
.slice(0, this.limit)
|
|
96
|
+
.map((run) => this.view(run))
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/** @return {string} The empty line. */
|
|
100
|
+
emptyText() {
|
|
101
|
+
const own = this.content && this.content.emptyText
|
|
102
|
+
return own ? this.cnTranslate(own) : t('nextcloud-vue', 'You have no exports here.')
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
mounted() {
|
|
107
|
+
this.load()
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
methods: {
|
|
111
|
+
t,
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Read the reader's export runs.
|
|
115
|
+
*
|
|
116
|
+
* @return {Promise<void>}
|
|
117
|
+
*/
|
|
118
|
+
async load() {
|
|
119
|
+
const user = getCurrentUser()
|
|
120
|
+
if (!user || !user.uid) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
this.loading = true
|
|
124
|
+
this.failed = false
|
|
125
|
+
try {
|
|
126
|
+
const params = { actor: user.uid, limit: Math.max(this.limit * 2, 10) }
|
|
127
|
+
for (const key of ['register', 'schema']) {
|
|
128
|
+
if (this.content && typeof this.content[key] === 'string' && this.content[key] !== '') {
|
|
129
|
+
params[key] = this.content[key]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const response = await axios.get(generateUrl('/apps/openregister/api/exports'), { params })
|
|
133
|
+
const results = response && response.data && Array.isArray(response.data.results) ? response.data.results : []
|
|
134
|
+
this.runs = [...results].sort((a, b) => String(b.producedAt || b.created || '').localeCompare(String(a.producedAt || a.created || '')))
|
|
135
|
+
} catch {
|
|
136
|
+
this.failed = true
|
|
137
|
+
this.runs = []
|
|
138
|
+
} finally {
|
|
139
|
+
this.loading = false
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* One export as the list draws it.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} run An export run.
|
|
147
|
+
* @return {{key: string, filename: string, meta: string, href: string, expiresToday: boolean}}
|
|
148
|
+
*/
|
|
149
|
+
view(run) {
|
|
150
|
+
const made = run.producedAt || run.created
|
|
151
|
+
const parts = []
|
|
152
|
+
if (made) {
|
|
153
|
+
parts.push(t('nextcloud-vue', 'Made {date}', { date: formatBoardDate(made, this.now) }))
|
|
154
|
+
}
|
|
155
|
+
if (typeof run.downloadCount === 'number') {
|
|
156
|
+
parts.push(n('nextcloud-vue', 'downloaded %n time', 'downloaded %n times', run.downloadCount))
|
|
157
|
+
}
|
|
158
|
+
if (run.expiresAt) {
|
|
159
|
+
parts.push(t('nextcloud-vue', 'expires {date}', { date: formatBoardDate(run.expiresAt, this.now) }))
|
|
160
|
+
}
|
|
161
|
+
const user = getCurrentUser()
|
|
162
|
+
const href = run.filePath && user && user.uid
|
|
163
|
+
? generateRemoteUrl(`dav/files/${encodeURIComponent(user.uid)}`) + String(run.filePath).split('/').map(encodeURIComponent).join('/')
|
|
164
|
+
: ''
|
|
165
|
+
return {
|
|
166
|
+
key: String(run.uuid || run.id),
|
|
167
|
+
filename: run.filename,
|
|
168
|
+
meta: parts.join(' · '),
|
|
169
|
+
href,
|
|
170
|
+
expiresToday: Boolean(run.expiresAt) && new Date(run.expiresAt).toDateString() === this.now.toDateString(),
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<style scoped>
|
|
178
|
+
.cn-exports-widget__list {
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
margin: 0;
|
|
182
|
+
padding: 0;
|
|
183
|
+
list-style: none;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.cn-exports-widget__row {
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
gap: 12px;
|
|
190
|
+
padding: 10px 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.cn-exports-widget__row + .cn-exports-widget__row {
|
|
194
|
+
border-top: 1px solid var(--color-border);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.cn-exports-widget__text {
|
|
198
|
+
display: flex;
|
|
199
|
+
flex: 1;
|
|
200
|
+
flex-direction: column;
|
|
201
|
+
gap: 2px;
|
|
202
|
+
min-width: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.cn-exports-widget__name {
|
|
206
|
+
overflow: hidden;
|
|
207
|
+
font-size: 14px;
|
|
208
|
+
font-weight: 600;
|
|
209
|
+
text-overflow: ellipsis;
|
|
210
|
+
white-space: nowrap;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.cn-exports-widget__meta,
|
|
214
|
+
.cn-exports-widget__muted {
|
|
215
|
+
color: var(--color-text-maxcontrast);
|
|
216
|
+
font-size: 13px;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.cn-exports-widget__muted {
|
|
220
|
+
margin: 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.cn-exports-widget__tag {
|
|
224
|
+
flex: none;
|
|
225
|
+
padding: 3px 10px;
|
|
226
|
+
border-radius: 12px;
|
|
227
|
+
background: var(--color-warning-light, color-mix(in srgb, var(--color-warning) 18%, transparent));
|
|
228
|
+
color: var(--color-warning-text, var(--color-main-text));
|
|
229
|
+
font-size: 13px;
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
white-space: nowrap;
|
|
232
|
+
}
|
|
233
|
+
</style>
|
|
@@ -113,6 +113,22 @@
|
|
|
113
113
|
{{ hostLabel(action) }}
|
|
114
114
|
</NcActionButton>
|
|
115
115
|
</NcActions>
|
|
116
|
+
<!-- The host's own buttons in the bar ("Vraag een bestand aan een
|
|
117
|
+
partij", DqZaakDocumenten), dispatched like a New-menu entry with
|
|
118
|
+
the current folder; `#header-actions` adds anything else. -->
|
|
119
|
+
<NcButton v-for="action in headerActions"
|
|
120
|
+
:key="`header-${action.id}`"
|
|
121
|
+
:variant="action.variant === 'primary' ? 'primary' : 'secondary'"
|
|
122
|
+
:disabled="folder === null"
|
|
123
|
+
:data-testid="`cn-files-browser-header-action-${action.id}`"
|
|
124
|
+
@click="runHostAction(action, null)">
|
|
125
|
+
<template v-if="action.icon" #icon>
|
|
126
|
+
<CnIcon :name="action.icon" :size="20" />
|
|
127
|
+
</template>
|
|
128
|
+
{{ hostLabel(action) }}
|
|
129
|
+
</NcButton>
|
|
130
|
+
<!-- @slot header-actions Buttons in the bar, before the upload button. -->
|
|
131
|
+
<slot name="header-actions" />
|
|
116
132
|
<!-- A plain upload as its own button, when the host names it
|
|
117
133
|
(`uploadButton`). It takes the primary place and the New
|
|
118
134
|
menu steps back beside it. -->
|
|
@@ -147,6 +163,31 @@
|
|
|
147
163
|
</li>
|
|
148
164
|
</ul>
|
|
149
165
|
|
|
166
|
+
<!-- The review summary (`reviewSummary`): how many files are reviewed,
|
|
167
|
+
a bar in the review tones and a legend with the count per value. -->
|
|
168
|
+
<div v-if="review && review.total > 0 && !loading && error === ''"
|
|
169
|
+
class="cn-files-browser__review"
|
|
170
|
+
data-testid="cn-files-browser-review">
|
|
171
|
+
<div class="cn-files-browser__review-head">
|
|
172
|
+
<strong class="cn-files-browser__review-title">{{ reviewTitle }}</strong>
|
|
173
|
+
<span v-if="reviewSubtitle" class="cn-files-browser__review-sub">{{ reviewSubtitle }}</span>
|
|
174
|
+
</div>
|
|
175
|
+
<span class="cn-files-browser__review-bar" role="img" :aria-label="reviewTitle">
|
|
176
|
+
<span v-for="segment in review.segments"
|
|
177
|
+
v-show="segment.count > 0 && !isOpenValue(segment.value)"
|
|
178
|
+
:key="segment.value"
|
|
179
|
+
:class="`cn-files-browser__review-part cn-files-browser__review-part--${segment.tone}`"
|
|
180
|
+
:style="{ width: `${segment.share}%` }" />
|
|
181
|
+
</span>
|
|
182
|
+
<ul class="cn-files-browser__review-legend">
|
|
183
|
+
<li v-for="segment in review.segments" :key="segment.value" data-testid="cn-files-browser-review-entry">
|
|
184
|
+
<span :class="`cn-files-browser__review-dot cn-files-browser__review-part--${segment.tone}`" aria-hidden="true" />
|
|
185
|
+
{{ hostLabel(segment) }}
|
|
186
|
+
<strong>{{ segment.count }}</strong>
|
|
187
|
+
</li>
|
|
188
|
+
</ul>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
150
191
|
<!-- The host's filter: per declared column, a chip per value in use
|
|
151
192
|
with how many files carry it. Chips narrow the list. -->
|
|
152
193
|
<div v-if="facetGroups.length > 0 && !loading && error === ''"
|
|
@@ -330,6 +371,9 @@
|
|
|
330
371
|
:class="`cn-files-browser__col-${column.key}`"
|
|
331
372
|
:data-testid="`cn-files-browser-cell-${column.key}`">
|
|
332
373
|
<span v-if="column.key === 'name'" class="cn-files-browser__name">{{ node.basename }}</span>
|
|
374
|
+
<span v-if="column.key === 'name' && nameSecondaryOf(node)"
|
|
375
|
+
class="cn-files-browser__name-secondary"
|
|
376
|
+
data-testid="cn-files-browser-name-secondary">{{ nameSecondaryOf(node) }}</span>
|
|
333
377
|
<template v-else-if="column.key === 'size'">
|
|
334
378
|
{{ isFolder(node) ? '' : formatSize(node.size) }}
|
|
335
379
|
</template>
|
|
@@ -347,6 +391,16 @@
|
|
|
347
391
|
:row="node" />
|
|
348
392
|
</td>
|
|
349
393
|
<td class="cn-files-browser__col-actions" @click.stop @auxclick.stop>
|
|
394
|
+
<!-- The host's one visible button on a file ("Beoordelen"),
|
|
395
|
+
primary while the row still asks for it. -->
|
|
396
|
+
<NcButton v-if="rowButton && !isFolder(node) && hostActionApplies(rowButton, node)"
|
|
397
|
+
:variant="rowButtonPrimary(node) ? 'primary' : 'secondary'"
|
|
398
|
+
class="cn-files-browser__row-button"
|
|
399
|
+
:aria-label="t('nextcloud-vue', '{action}: {name}', { action: hostLabel(rowButton), name: node.basename })"
|
|
400
|
+
data-testid="cn-files-browser-row-button"
|
|
401
|
+
@click="runHostAction(rowButton, node)">
|
|
402
|
+
{{ hostLabel(rowButton) }}
|
|
403
|
+
</NcButton>
|
|
350
404
|
<NcActions :forceMenu="true" :ariaLabel="t('nextcloud-vue', 'Actions for {name}', { name: node.basename })">
|
|
351
405
|
<NcActionButton
|
|
352
406
|
v-for="action in actionsFor(node)"
|
|
@@ -548,6 +602,14 @@
|
|
|
548
602
|
<Upload :size="16" />
|
|
549
603
|
{{ dropHintLabel }}
|
|
550
604
|
</p>
|
|
605
|
+
<!-- The host's own line under the list ("Elk bestand wordt bij het
|
|
606
|
+
uploaden op virussen gescand", DqDocumentenBeheer), apart from
|
|
607
|
+
the drop hint. -->
|
|
608
|
+
<p v-if="footnote && !loading && error === ''"
|
|
609
|
+
class="cn-files-browser__footnote"
|
|
610
|
+
data-testid="cn-files-browser-footnote">
|
|
611
|
+
{{ cnTranslate(footnote) }}
|
|
612
|
+
</p>
|
|
551
613
|
|
|
552
614
|
<NcDialog
|
|
553
615
|
v-if="renaming !== null"
|
|
@@ -688,6 +750,7 @@ import CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'
|
|
|
688
750
|
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
689
751
|
import { readUserPreference, writeUserPreference } from '../../composables/useUserPreferences.js'
|
|
690
752
|
import { dispatchAction } from '../../utils/actionsDispatcher.js'
|
|
753
|
+
import { fillTemplate } from '../../utils/fillTemplate.js'
|
|
691
754
|
import { isNewTabClick, isRowMiddleClick, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
|
|
692
755
|
import { ACTIONS_NEEDING_THE_FILES_PAGE, crumbsFor, hostActionApplies, joinPath } from './filesBrowser.js'
|
|
693
756
|
import {
|
|
@@ -700,6 +763,7 @@ import {
|
|
|
700
763
|
sortNodesByColumn,
|
|
701
764
|
visibleFileColumns,
|
|
702
765
|
} from './filesBrowserColumns.js'
|
|
766
|
+
import { reviewSummary } from './filesReview.js'
|
|
703
767
|
|
|
704
768
|
let uploadSeq = 0
|
|
705
769
|
|
|
@@ -811,6 +875,50 @@ export default {
|
|
|
811
875
|
default: () => [],
|
|
812
876
|
},
|
|
813
877
|
|
|
878
|
+
/**
|
|
879
|
+
* The host's own buttons in the bar, before the upload button ("Vraag
|
|
880
|
+
* een bestand aan een partij", DqZaakDocumenten). Each is dispatched
|
|
881
|
+
* like a New-menu entry, with the current folder; `variant:
|
|
882
|
+
* 'primary'` draws it as the primary button.
|
|
883
|
+
*
|
|
884
|
+
* @type {Array<{id: string, label: string, icon?: string, variant?: string, type?: string, target?: string, props?: object, handler?: string, args?: Array}>}
|
|
885
|
+
*/
|
|
886
|
+
headerActions: {
|
|
887
|
+
type: Array,
|
|
888
|
+
default: () => [],
|
|
889
|
+
},
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* One visible button on every file row ("Beoordelen"), dispatched like a
|
|
893
|
+
* row action. `primaryWhen: { key, values }` draws it as the primary
|
|
894
|
+
* button while the row-data field `key` holds one of `values` (a file
|
|
895
|
+
* still to review), and secondary otherwise. `visibleIf` works as on a
|
|
896
|
+
* row action.
|
|
897
|
+
*
|
|
898
|
+
* @type {{id: string, label: string, type?: string, target?: string, props?: object, handler?: string, args?: Array, primaryWhen?: {key: string, values: Array<string>}, visibleIf?: object}|null}
|
|
899
|
+
*/
|
|
900
|
+
rowButton: {
|
|
901
|
+
type: Object,
|
|
902
|
+
default: null,
|
|
903
|
+
},
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* A review summary above the list (DqZaakDocumenten "4 van 7
|
|
907
|
+
* beoordeeld"): `{ key, open, values: [{ value, label?, tone? }], title?,
|
|
908
|
+
* subtitle? }`. `key` is the row-data field with the review state, `open`
|
|
909
|
+
* the value (or values) of a file still to review; a file without a
|
|
910
|
+
* listed value counts as open. `tone` is `success`, `warning`, `error`,
|
|
911
|
+
* `info` or `default`. `title` and `subtitle` take `{done}`, `{total}` and
|
|
912
|
+
* `{open}`, through the host's translate; they default to the library's
|
|
913
|
+
* "{done} of {total} reviewed" and "{open} still to review".
|
|
914
|
+
*
|
|
915
|
+
* @type {{key: string, open?: string|Array<string>, values: Array<{value: string, label?: string, tone?: string}>, title?: string, subtitle?: string}|null}
|
|
916
|
+
*/
|
|
917
|
+
reviewSummary: {
|
|
918
|
+
type: Object,
|
|
919
|
+
default: null,
|
|
920
|
+
},
|
|
921
|
+
|
|
814
922
|
/**
|
|
815
923
|
* Rows that are not nodes of this folder: files the host joined from
|
|
816
924
|
* another object's folder. Rendered after the folder's own rows with
|
|
@@ -930,6 +1038,29 @@ export default {
|
|
|
930
1038
|
default: false,
|
|
931
1039
|
},
|
|
932
1040
|
|
|
1041
|
+
/**
|
|
1042
|
+
* A line of the host's own under the list, apart from the drop hint
|
|
1043
|
+
* ("Elk bestand wordt bij het uploaden op virussen gescand en krijgt een
|
|
1044
|
+
* controlegetal", DqDocumentenBeheer). Translated with the host's
|
|
1045
|
+
* catalogue.
|
|
1046
|
+
*/
|
|
1047
|
+
footnote: {
|
|
1048
|
+
type: String,
|
|
1049
|
+
default: '',
|
|
1050
|
+
},
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* A second line under each file's name (DqDocumentenBeheer "Inwoner ·
|
|
1054
|
+
* 4 okt"): a field template read from the file's row data, plus
|
|
1055
|
+
* `name`, `modified` (the change date) and `owner` (the owner's display
|
|
1056
|
+
* name), e.g. `"{uploadedBy} · {modified|date}"`. A line with an empty
|
|
1057
|
+
* value is left out. Translated with the host's catalogue first.
|
|
1058
|
+
*/
|
|
1059
|
+
nameSecondary: {
|
|
1060
|
+
type: String,
|
|
1061
|
+
default: '',
|
|
1062
|
+
},
|
|
1063
|
+
|
|
933
1064
|
/** The line `dropHint` shows. */
|
|
934
1065
|
dropHintLabel: {
|
|
935
1066
|
type: String,
|
|
@@ -1331,6 +1462,28 @@ export default {
|
|
|
1331
1462
|
return this.sorted.filter((node) => !this.isFolder(node))
|
|
1332
1463
|
},
|
|
1333
1464
|
|
|
1465
|
+
/**
|
|
1466
|
+
* The review counts over the folder's files, or null without a summary.
|
|
1467
|
+
*
|
|
1468
|
+
* @return {object|null}
|
|
1469
|
+
*/
|
|
1470
|
+
review() {
|
|
1471
|
+
return reviewSummary(this.reviewSummary, this.fileNodes, this.resolvedRowData)
|
|
1472
|
+
},
|
|
1473
|
+
|
|
1474
|
+
/** @return {string} The summary's title, counts filled in. */
|
|
1475
|
+
reviewTitle() {
|
|
1476
|
+
return this.reviewText('title', t('nextcloud-vue', '{done} of {total} reviewed'))
|
|
1477
|
+
},
|
|
1478
|
+
|
|
1479
|
+
/** @return {string} The summary's second line, or '' when nothing is open. */
|
|
1480
|
+
reviewSubtitle() {
|
|
1481
|
+
if (!this.review || this.review.open === 0) {
|
|
1482
|
+
return ''
|
|
1483
|
+
}
|
|
1484
|
+
return this.reviewText('subtitle', t('nextcloud-vue', '{open} still to review'))
|
|
1485
|
+
},
|
|
1486
|
+
|
|
1334
1487
|
/**
|
|
1335
1488
|
* Whether any facet value is chosen.
|
|
1336
1489
|
*
|
|
@@ -1793,6 +1946,73 @@ export default {
|
|
|
1793
1946
|
return this.cnTranslate(String(action?.label ?? ''))
|
|
1794
1947
|
},
|
|
1795
1948
|
|
|
1949
|
+
/**
|
|
1950
|
+
* The second line under a file's name, or '' without `nameSecondary`,
|
|
1951
|
+
* for a folder, or when a value in it is empty.
|
|
1952
|
+
*
|
|
1953
|
+
* @param {object} node The file.
|
|
1954
|
+
* @return {string}
|
|
1955
|
+
*/
|
|
1956
|
+
nameSecondaryOf(node) {
|
|
1957
|
+
if (!this.nameSecondary || this.isFolder(node)) {
|
|
1958
|
+
return ''
|
|
1959
|
+
}
|
|
1960
|
+
const byId = this.resolvedRowData && typeof this.resolvedRowData === 'object' ? this.resolvedRowData : {}
|
|
1961
|
+
const entry = byId[node.fileid] ?? byId[String(node.fileid)] ?? {}
|
|
1962
|
+
const owner = node.attributes ? node.attributes['owner-display-name'] : undefined
|
|
1963
|
+
const object = { ...entry, name: node.basename, modified: node.mtime, ...(owner ? { owner } : {}) }
|
|
1964
|
+
return fillTemplate(this.cnTranslate(this.nameSecondary), object)
|
|
1965
|
+
},
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* A review summary line: the host's template through its translate, else
|
|
1969
|
+
* the library's, with the counts filled in.
|
|
1970
|
+
*
|
|
1971
|
+
* @param {string} key `title` or `subtitle`.
|
|
1972
|
+
* @param {string} fallback The library's line, already translated.
|
|
1973
|
+
* @return {string}
|
|
1974
|
+
*/
|
|
1975
|
+
reviewText(key, fallback) {
|
|
1976
|
+
const own = this.reviewSummary && typeof this.reviewSummary[key] === 'string' && this.reviewSummary[key] !== ''
|
|
1977
|
+
? this.cnTranslate(this.reviewSummary[key])
|
|
1978
|
+
: fallback
|
|
1979
|
+
const counts = this.review || { done: 0, total: 0, open: 0 }
|
|
1980
|
+
return own.replace(/\{(done|total|open)\}/g, (match, name) => String(counts[name]))
|
|
1981
|
+
},
|
|
1982
|
+
|
|
1983
|
+
/**
|
|
1984
|
+
* Whether a review value is one of the summary's open values.
|
|
1985
|
+
*
|
|
1986
|
+
* @param {string} value The value.
|
|
1987
|
+
* @return {boolean}
|
|
1988
|
+
*/
|
|
1989
|
+
isOpenValue(value) {
|
|
1990
|
+
const open = this.reviewSummary ? this.reviewSummary.open : undefined
|
|
1991
|
+
const list = (Array.isArray(open) ? open : [open]).filter((v) => v !== undefined && v !== null)
|
|
1992
|
+
return list.some((v) => String(v).toLowerCase() === String(value).toLowerCase())
|
|
1993
|
+
},
|
|
1994
|
+
|
|
1995
|
+
/**
|
|
1996
|
+
* Whether a file's row button is the primary one: the row-data field
|
|
1997
|
+
* `primaryWhen.key` holds one of `primaryWhen.values`.
|
|
1998
|
+
*
|
|
1999
|
+
* @param {object} node The file.
|
|
2000
|
+
* @return {boolean}
|
|
2001
|
+
*/
|
|
2002
|
+
rowButtonPrimary(node) {
|
|
2003
|
+
const when = this.rowButton && this.rowButton.primaryWhen
|
|
2004
|
+
if (!when || typeof when.key !== 'string' || !Array.isArray(when.values)) {
|
|
2005
|
+
return false
|
|
2006
|
+
}
|
|
2007
|
+
const raw = fileColumnValue({ key: when.key, source: 'row' }, node, this.resolvedRowData)
|
|
2008
|
+
if (raw === null || raw === undefined) {
|
|
2009
|
+
return false
|
|
2010
|
+
}
|
|
2011
|
+
return when.values.some((v) => String(v).toLowerCase() === String(raw).toLowerCase())
|
|
2012
|
+
},
|
|
2013
|
+
|
|
2014
|
+
hostActionApplies,
|
|
2015
|
+
|
|
1796
2016
|
/**
|
|
1797
2017
|
* Re-read the folder once after a burst of change signals. One upload
|
|
1798
2018
|
* of ten files answers ten `files:node:created`, and one PROPFIND
|
|
@@ -2938,6 +3158,89 @@ export default {
|
|
|
2938
3158
|
margin-inline-end: 8px;
|
|
2939
3159
|
}
|
|
2940
3160
|
|
|
3161
|
+
.cn-files-browser__review {
|
|
3162
|
+
display: flex;
|
|
3163
|
+
flex-direction: column;
|
|
3164
|
+
gap: 10px;
|
|
3165
|
+
margin-block-end: 12px;
|
|
3166
|
+
padding: 14px 16px;
|
|
3167
|
+
border-radius: 10px;
|
|
3168
|
+
background: var(--color-background-hover);
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
.cn-files-browser__review-head {
|
|
3172
|
+
display: flex;
|
|
3173
|
+
flex-wrap: wrap;
|
|
3174
|
+
align-items: baseline;
|
|
3175
|
+
justify-content: space-between;
|
|
3176
|
+
gap: 8px;
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
.cn-files-browser__review-title {
|
|
3180
|
+
font-size: 15px;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
.cn-files-browser__review-sub {
|
|
3184
|
+
color: var(--color-text-maxcontrast);
|
|
3185
|
+
font-size: 13px;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
.cn-files-browser__review-bar {
|
|
3189
|
+
display: flex;
|
|
3190
|
+
height: 8px;
|
|
3191
|
+
overflow: hidden;
|
|
3192
|
+
border-radius: 4px;
|
|
3193
|
+
background: var(--color-border);
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
.cn-files-browser__review-legend {
|
|
3197
|
+
display: flex;
|
|
3198
|
+
flex-wrap: wrap;
|
|
3199
|
+
gap: 8px 18px;
|
|
3200
|
+
margin: 0;
|
|
3201
|
+
padding: 0;
|
|
3202
|
+
font-size: 14px;
|
|
3203
|
+
list-style: none;
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
.cn-files-browser__review-legend li {
|
|
3207
|
+
display: inline-flex;
|
|
3208
|
+
align-items: center;
|
|
3209
|
+
gap: 8px;
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
.cn-files-browser__review-dot {
|
|
3213
|
+
width: 10px;
|
|
3214
|
+
height: 10px;
|
|
3215
|
+
border-radius: 5px;
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
.cn-files-browser__review-part--success {
|
|
3219
|
+
background: var(--color-success-text, var(--color-success));
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
.cn-files-browser__review-part--warning {
|
|
3223
|
+
background: var(--color-warning);
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
.cn-files-browser__review-part--error {
|
|
3227
|
+
background: var(--color-error-text, var(--color-error));
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
.cn-files-browser__review-part--info {
|
|
3231
|
+
background: var(--color-primary-element);
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
.cn-files-browser__review-part--default {
|
|
3235
|
+
background: var(--color-border-dark);
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
.cn-files-browser__row-button {
|
|
3239
|
+
display: inline-flex;
|
|
3240
|
+
margin-inline-end: 8px;
|
|
3241
|
+
vertical-align: middle;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
2941
3244
|
.cn-files-browser__facets {
|
|
2942
3245
|
display: flex;
|
|
2943
3246
|
flex-wrap: wrap;
|
|
@@ -2988,6 +3291,11 @@ export default {
|
|
|
2988
3291
|
|
|
2989
3292
|
.cn-files-browser__col-actions {
|
|
2990
3293
|
width: 48px;
|
|
3294
|
+
white-space: nowrap;
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
.cn-files-browser__col-actions:has(.cn-files-browser__row-button) {
|
|
3298
|
+
width: auto;
|
|
2991
3299
|
}
|
|
2992
3300
|
|
|
2993
3301
|
.cn-files-browser__col-name {
|
|
@@ -3001,6 +3309,22 @@ export default {
|
|
|
3001
3309
|
white-space: nowrap;
|
|
3002
3310
|
}
|
|
3003
3311
|
|
|
3312
|
+
.cn-files-browser__name-secondary {
|
|
3313
|
+
display: block;
|
|
3314
|
+
overflow: hidden;
|
|
3315
|
+
color: var(--color-text-maxcontrast);
|
|
3316
|
+
font-size: 13px;
|
|
3317
|
+
text-overflow: ellipsis;
|
|
3318
|
+
white-space: nowrap;
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
.cn-files-browser__footnote {
|
|
3322
|
+
margin: 0;
|
|
3323
|
+
padding-top: 12px;
|
|
3324
|
+
color: var(--color-text-maxcontrast);
|
|
3325
|
+
font-size: 13px;
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3004
3328
|
.cn-files-browser__row--linked {
|
|
3005
3329
|
color: var(--color-text-maxcontrast);
|
|
3006
3330
|
}
|