@conduction/nextcloud-vue 2.60.0 → 2.61.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 +41 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +9 -2
- 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/CnDashboardPage/CnDashboardPage.vue2.js +10 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +6 -5
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +3 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +10 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js.map +1 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +316 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +105 -0
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -0
- package/dist/esm/components/CnPageRenderer/pageTypes.js +1 -0
- package/dist/esm/components/CnPageRenderer/pageTypes.js.map +1 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +1 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +1 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +92 -2
- package/dist/esm/utils/headers.js +28 -2
- package/dist/esm/utils/headers.js.map +1 -1
- 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 +8741 -8229
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +92 -2
- package/l10n/en.json +1 -0
- package/l10n/nl.json +1 -0
- package/package.json +1 -1
- package/scripts/.jsdoc-baselines.json +1 -0
- package/src/components/CnAppNav/CnAppNav.vue +41 -1
- package/src/components/CnBannerWidget/CnBannerWidget.vue +9 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +10 -2
- package/src/components/CnDetailPage/CnDetailPage.vue +4 -1
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +3 -1
- package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +484 -0
- package/src/components/CnLinkCardsPage/index.js +4 -0
- package/src/components/CnPageRenderer/pageTypes.js +1 -0
- package/src/components/index.js +1 -0
- package/src/css/detail-page.css +10 -2
- package/src/index.js +7 -0
- package/src/schemas/app-manifest-v2.schema.json +44 -2
- package/src/types/manifest.d.ts +1 -1
- package/src/utils/headers.js +28 -2
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { translate } from '@nextcloud/l10n';
|
|
2
|
+
import { isAppInstalled } from '../../utils/appInstalled.js';
|
|
3
|
+
import { resolveHref, followLinkClick } from '../../utils/linkNavigation.js';
|
|
4
|
+
import { passesContextPredicates } from '../../utils/visibleIfContext.js';
|
|
5
|
+
import __sfc_main from '../CnIcon/CnIcon.vue.js';
|
|
6
|
+
|
|
7
|
+
let linkCardsPageUid = 0;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CnLinkCardsPage: the `type: "links"` page. Link cards, grouped under captions.
|
|
11
|
+
*
|
|
12
|
+
* One page that opens every page a short menu leaves out. A card has a
|
|
13
|
+
* label, a one-line description, an optional icon and a target. Groups come
|
|
14
|
+
* from `categories`, in the order they are declared; a card names its group
|
|
15
|
+
* with `category`.
|
|
16
|
+
*
|
|
17
|
+
* ```json
|
|
18
|
+
* { "id": "Modules", "route": "/modules", "type": "links", "title": "Modules and more",
|
|
19
|
+
* "config": {
|
|
20
|
+
* "description": "Every page that is not in the daily menu.",
|
|
21
|
+
* "categories": { "sales": "Sales", "marketing": "Marketing" },
|
|
22
|
+
* "cards": [
|
|
23
|
+
* { "id": "Leads", "label": "Leads", "description": "Every deal you are working on.",
|
|
24
|
+
* "icon": "CashMultiple", "category": "sales", "route": "Leads" }
|
|
25
|
+
* ]
|
|
26
|
+
* } }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Every string goes through the app's translate function. A card is hidden
|
|
30
|
+
* by the same `visibleIf` and `permission` a menu entry takes, and a group
|
|
31
|
+
* with no visible card is not drawn.
|
|
32
|
+
*/
|
|
33
|
+
var script = {
|
|
34
|
+
name: 'CnLinkCardsPage',
|
|
35
|
+
|
|
36
|
+
components: { CnIcon: __sfc_main },
|
|
37
|
+
|
|
38
|
+
inject: {
|
|
39
|
+
/** The app's translate function, provided by CnAppRoot. Identity outside one. */
|
|
40
|
+
cnTranslate: { default: () => (key) => key },
|
|
41
|
+
/** The effective manifest, for `visibleIf` predicates against `runtime`. */
|
|
42
|
+
cnManifest: { default: null },
|
|
43
|
+
/** The permissions the user holds. Empty means the app did not say, and allows. */
|
|
44
|
+
cnPermissions: { default: () => [] },
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
props: {
|
|
48
|
+
/** The whole manifest page, for a host that mounts this component itself. CnPageRenderer passes the flattened props below instead. */
|
|
49
|
+
page: {
|
|
50
|
+
type: Object,
|
|
51
|
+
default: () => ({}),
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The cards: `{ id, label, description?, icon?, category?, route?, params?, query?, href?, visibleIf?, permission? }`.
|
|
56
|
+
* `route` is a route name, or a path when it starts with `/`.
|
|
57
|
+
*
|
|
58
|
+
* @type {Array<object>|null}
|
|
59
|
+
*/
|
|
60
|
+
cards: {
|
|
61
|
+
type: Array,
|
|
62
|
+
default: null,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Group key to caption. Sets the order of the groups.
|
|
67
|
+
*
|
|
68
|
+
* @type {Record<string, string>|null}
|
|
69
|
+
*/
|
|
70
|
+
categories: {
|
|
71
|
+
type: Object,
|
|
72
|
+
default: null,
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/** The heading. */
|
|
76
|
+
title: {
|
|
77
|
+
type: String,
|
|
78
|
+
default: null,
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/** The lead paragraph under the heading. */
|
|
82
|
+
description: {
|
|
83
|
+
type: String,
|
|
84
|
+
default: null,
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/** Text shown when no card is visible. */
|
|
88
|
+
emptyLabel: {
|
|
89
|
+
type: String,
|
|
90
|
+
default: null,
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Translate function. Falls back to the injected `cnTranslate`.
|
|
95
|
+
*
|
|
96
|
+
* @type {((key: string) => string)|null}
|
|
97
|
+
*/
|
|
98
|
+
translate: {
|
|
99
|
+
type: Function,
|
|
100
|
+
default: null,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
data() {
|
|
105
|
+
linkCardsPageUid += 1;
|
|
106
|
+
return {
|
|
107
|
+
uid: linkCardsPageUid,
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
computed: {
|
|
112
|
+
/**
|
|
113
|
+
* The config, from the flattened props or from `page.config`. Props win.
|
|
114
|
+
*
|
|
115
|
+
* @return {object} The config.
|
|
116
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards
|
|
117
|
+
*/
|
|
118
|
+
config() {
|
|
119
|
+
const fromPage = (this.page && this.page.config) || {};
|
|
120
|
+
const fromProps = {};
|
|
121
|
+
for (const key of ['cards', 'categories', 'description', 'title', 'emptyLabel']) {
|
|
122
|
+
if (this[key] !== null && this[key] !== undefined) {
|
|
123
|
+
fromProps[key] = this[key];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { ...fromPage, ...fromProps }
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The heading, translated.
|
|
131
|
+
*
|
|
132
|
+
* @return {string} The title, or '' when the page declares none.
|
|
133
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards
|
|
134
|
+
*/
|
|
135
|
+
resolvedTitle() {
|
|
136
|
+
return this.tr(this.config.title || (this.page && this.page.title) || '')
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The lead paragraph, translated.
|
|
141
|
+
*
|
|
142
|
+
* @return {string} The description.
|
|
143
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards
|
|
144
|
+
*/
|
|
145
|
+
resolvedDescription() {
|
|
146
|
+
return this.tr(this.config.description || '')
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The empty-state text.
|
|
151
|
+
*
|
|
152
|
+
* @return {string} The text.
|
|
153
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards
|
|
154
|
+
*/
|
|
155
|
+
resolvedEmptyLabel() {
|
|
156
|
+
return this.config.emptyLabel ? this.tr(this.config.emptyLabel) : translate('nextcloud-vue', 'Nothing to open here.')
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The cards a reader may see and can open, with text translated and
|
|
161
|
+
* the link resolved. A card that is gated away, or has no target that
|
|
162
|
+
* resolves, is left out: a card that does nothing is worse than none.
|
|
163
|
+
*
|
|
164
|
+
* @return {Array<object>} The cards.
|
|
165
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link
|
|
166
|
+
*/
|
|
167
|
+
visibleCards() {
|
|
168
|
+
const declared = Array.isArray(this.config.cards) ? this.config.cards : [];
|
|
169
|
+
const runtime = this.cnManifest?.runtime ?? null;
|
|
170
|
+
return declared
|
|
171
|
+
.filter((card) => card && card.id && card.label)
|
|
172
|
+
.filter((card) => this.passesPermission(card) && this.passesVisibleIf(card, runtime))
|
|
173
|
+
.map((card) => {
|
|
174
|
+
const target = this.targetOf(card);
|
|
175
|
+
return {
|
|
176
|
+
id: String(card.id),
|
|
177
|
+
label: this.tr(card.label),
|
|
178
|
+
description: this.tr(card.description || ''),
|
|
179
|
+
descriptionId: `cn-link-card-desc-${this.uid}-${String(card.id).replace(/[^A-Za-z0-9_-]/g, '-')}`,
|
|
180
|
+
icon: card.icon || '',
|
|
181
|
+
category: card.category || '',
|
|
182
|
+
external: target.external,
|
|
183
|
+
location: target.location,
|
|
184
|
+
url: target.url,
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
.filter((card) => card.url !== '')
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The groups to draw: ungrouped cards first under no caption, then
|
|
192
|
+
* one group per category in declaration order. A group without a
|
|
193
|
+
* visible card is left out.
|
|
194
|
+
*
|
|
195
|
+
* @return {Array<{key: string, label: string, headingId: string, cards: Array<object>}>} The groups.
|
|
196
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards
|
|
197
|
+
*/
|
|
198
|
+
groups() {
|
|
199
|
+
const declared = (this.config.categories && typeof this.config.categories === 'object') ? this.config.categories : {};
|
|
200
|
+
const keys = Object.keys(declared);
|
|
201
|
+
const loose = this.visibleCards.filter((card) => !keys.includes(card.category));
|
|
202
|
+
const groups = loose.length > 0 ? [{ key: '', label: '', headingId: '', cards: loose }] : [];
|
|
203
|
+
keys.forEach((key, index) => {
|
|
204
|
+
const cards = this.visibleCards.filter((card) => card.category === key);
|
|
205
|
+
if (cards.length > 0) {
|
|
206
|
+
groups.push({
|
|
207
|
+
key,
|
|
208
|
+
label: this.tr(declared[key]),
|
|
209
|
+
headingId: `cn-link-cards-group-${this.uid}-${index}`,
|
|
210
|
+
cards,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return groups
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
methods: {
|
|
219
|
+
/**
|
|
220
|
+
* Translate a manifest string through the app's translate function.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} value The declared string.
|
|
223
|
+
* @return {string} The translation, or the input unchanged.
|
|
224
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards
|
|
225
|
+
*/
|
|
226
|
+
tr(value) {
|
|
227
|
+
if (typeof value !== 'string' || value === '') {
|
|
228
|
+
return value
|
|
229
|
+
}
|
|
230
|
+
return (this.translate ?? this.cnTranslate)(value)
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Whether the user holds the card's `permission`. Same rule as the
|
|
235
|
+
* menu: no permission declared, or no permissions known, allows.
|
|
236
|
+
*
|
|
237
|
+
* @param {object} card The card.
|
|
238
|
+
* @return {boolean} True when the card may render.
|
|
239
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-cards-honour-menu-visibility-conditions
|
|
240
|
+
*/
|
|
241
|
+
passesPermission(card) {
|
|
242
|
+
const held = Array.isArray(this.cnPermissions) ? this.cnPermissions : [];
|
|
243
|
+
if (!card.permission || held.length === 0) {
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
246
|
+
return held.includes(card.permission)
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Evaluate a card's `visibleIf` the way CnAppNav evaluates a menu
|
|
251
|
+
* entry's: `appInstalled` first, then dot-path predicates against
|
|
252
|
+
* the manifest's `runtime`.
|
|
253
|
+
*
|
|
254
|
+
* @param {object} card The card.
|
|
255
|
+
* @param {object|null} runtime `manifest.runtime`, or null.
|
|
256
|
+
* @return {boolean} True when the card may render.
|
|
257
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-cards-honour-menu-visibility-conditions
|
|
258
|
+
*/
|
|
259
|
+
passesVisibleIf(card, runtime) {
|
|
260
|
+
const condition = card.visibleIf;
|
|
261
|
+
if (!condition || typeof condition !== 'object') {
|
|
262
|
+
return true
|
|
263
|
+
}
|
|
264
|
+
if (condition.appInstalled && !isAppInstalled(condition.appInstalled)) {
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
return passesContextPredicates(condition, runtime)
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Where a card goes. An `href` is an external URL. A `route` is a
|
|
272
|
+
* route name, or a path when it starts with `/`, resolved by the
|
|
273
|
+
* router so the link carries the app's base with or without
|
|
274
|
+
* `/index.php`.
|
|
275
|
+
*
|
|
276
|
+
* @param {object} card The card.
|
|
277
|
+
* @return {{external: boolean, location: object|null, url: string}} The target; `url` is '' when it cannot be resolved.
|
|
278
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link
|
|
279
|
+
*/
|
|
280
|
+
targetOf(card) {
|
|
281
|
+
if (typeof card.route === 'string' && card.route !== '') {
|
|
282
|
+
const location = card.route.startsWith('/') ? { path: card.route } : { name: card.route };
|
|
283
|
+
if (card.params && typeof card.params === 'object') {
|
|
284
|
+
location.params = card.params;
|
|
285
|
+
}
|
|
286
|
+
if (card.query && typeof card.query === 'object') {
|
|
287
|
+
location.query = card.query;
|
|
288
|
+
}
|
|
289
|
+
return { external: false, location, url: resolveHref(location, this.$router) }
|
|
290
|
+
}
|
|
291
|
+
if (typeof card.href === 'string' && card.href !== '') {
|
|
292
|
+
return { external: true, location: null, url: card.href }
|
|
293
|
+
}
|
|
294
|
+
return { external: false, location: null, url: '' }
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Route a plain click on an in-app card. A modified or middle click,
|
|
299
|
+
* and every click on an external card, is left to the browser.
|
|
300
|
+
*
|
|
301
|
+
* @param {object} card The resolved card.
|
|
302
|
+
* @param {MouseEvent} event The click.
|
|
303
|
+
* @return {void}
|
|
304
|
+
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link
|
|
305
|
+
*/
|
|
306
|
+
open(card, event) {
|
|
307
|
+
if (card.external || !card.location) {
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
followLinkClick(event, card.location, this.$router);
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
export { script as default };
|
|
316
|
+
//# sourceMappingURL=CnLinkCardsPage.vue2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnLinkCardsPage.vue2.js","sources":["../../../../src/components/CnLinkCardsPage/CnLinkCardsPage.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-link-cards-page\" data-testid=\"cn-link-cards-page\">\n\t\t<div class=\"cn-link-cards-page__header\">\n\t\t\t<h2 v-if=\"resolvedTitle\" class=\"cn-link-cards-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-link-cards-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<p\n\t\t\tv-if=\"groups.length === 0\"\n\t\t\tclass=\"cn-link-cards-page__empty\"\n\t\t\tdata-testid=\"cn-link-cards-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<section\n\t\t\tv-for=\"group in groups\"\n\t\t\t:key=\"group.key\"\n\t\t\tclass=\"cn-link-cards-page__group\"\n\t\t\t:aria-labelledby=\"group.label ? group.headingId : null\"\n\t\t\t:aria-label=\"group.label ? null : resolvedTitle || null\"\n\t\t\tdata-testid=\"cn-link-cards-group\"\n\t\t\t:data-group=\"group.key\">\n\t\t\t<h3\n\t\t\t\tv-if=\"group.label\"\n\t\t\t\t:id=\"group.headingId\"\n\t\t\t\tclass=\"cn-link-cards-page__caption\">\n\t\t\t\t{{ group.label }}\n\t\t\t</h3>\n\t\t\t<ul class=\"cn-link-cards-page__grid\">\n\t\t\t\t<li v-for=\"card in group.cards\" :key=\"card.id\" class=\"cn-link-cards-page__cell\">\n\t\t\t\t\t<!-- A real <a href>, so the card can be middle-clicked, copied\n\t\t\t\t\t and announced as a link. A plain click on an in-app card\n\t\t\t\t\t is routed, which keeps the page from reloading. -->\n\t\t\t\t\t<a\n\t\t\t\t\t\tclass=\"cn-link-cards-page__card\"\n\t\t\t\t\t\tdata-testid=\"cn-link-card\"\n\t\t\t\t\t\t:data-card-id=\"card.id\"\n\t\t\t\t\t\t:href=\"card.url\"\n\t\t\t\t\t\t:target=\"card.external ? '_blank' : null\"\n\t\t\t\t\t\t:rel=\"card.external ? 'noopener noreferrer' : null\"\n\t\t\t\t\t\t:aria-describedby=\"card.description ? card.descriptionId : null\"\n\t\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\tv-if=\"card.icon\"\n\t\t\t\t\t\t\t:name=\"card.icon\"\n\t\t\t\t\t\t\t:size=\"24\"\n\t\t\t\t\t\t\tclass=\"cn-link-cards-page__icon\"\n\t\t\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t\t\t<span class=\"cn-link-cards-page__body\">\n\t\t\t\t\t\t\t<span class=\"cn-link-cards-page__label\">{{ card.label }}</span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"card.description\"\n\t\t\t\t\t\t\t\t:id=\"card.descriptionId\"\n\t\t\t\t\t\t\t\tclass=\"cn-link-cards-page__card-description\">\n\t\t\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</a>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\nimport { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'\nimport { passesContextPredicates } from '../../utils/visibleIfContext.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\nlet linkCardsPageUid = 0\n\n/**\n * CnLinkCardsPage: the `type: \"links\"` page. Link cards, grouped under captions.\n *\n * One page that opens every page a short menu leaves out. A card has a\n * label, a one-line description, an optional icon and a target. Groups come\n * from `categories`, in the order they are declared; a card names its group\n * with `category`.\n *\n * ```json\n * { \"id\": \"Modules\", \"route\": \"/modules\", \"type\": \"links\", \"title\": \"Modules and more\",\n * \"config\": {\n * \"description\": \"Every page that is not in the daily menu.\",\n * \"categories\": { \"sales\": \"Sales\", \"marketing\": \"Marketing\" },\n * \"cards\": [\n * { \"id\": \"Leads\", \"label\": \"Leads\", \"description\": \"Every deal you are working on.\",\n * \"icon\": \"CashMultiple\", \"category\": \"sales\", \"route\": \"Leads\" }\n * ]\n * } }\n * ```\n *\n * Every string goes through the app's translate function. A card is hidden\n * by the same `visibleIf` and `permission` a menu entry takes, and a group\n * with no visible card is not drawn.\n */\nexport default {\n\tname: 'CnLinkCardsPage',\n\n\tcomponents: { CnIcon },\n\n\tinject: {\n\t\t/** The app's translate function, provided by CnAppRoot. Identity outside one. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The effective manifest, for `visibleIf` predicates against `runtime`. */\n\t\tcnManifest: { default: null },\n\t\t/** The permissions the user holds. Empty means the app did not say, and allows. */\n\t\tcnPermissions: { default: () => [] },\n\t},\n\n\tprops: {\n\t\t/** The whole manifest page, for a host that mounts this component itself. CnPageRenderer passes the flattened props below instead. */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The cards: `{ id, label, description?, icon?, category?, route?, params?, query?, href?, visibleIf?, permission? }`.\n\t\t * `route` is a route name, or a path when it starts with `/`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Group key to caption. Sets the order of the groups.\n\t\t *\n\t\t * @type {Record<string, string>|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The heading. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The lead paragraph under the heading. */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown when no card is visible. */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\tlinkCardsPageUid += 1\n\t\treturn {\n\t\t\tuid: linkCardsPageUid,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The config, from the flattened props or from `page.config`. Props win.\n\t\t *\n\t\t * @return {object} The config.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of ['cards', 'categories', 'description', 'title', 'emptyLabel']) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The heading, translated.\n\t\t *\n\t\t * @return {string} The title, or '' when the page declares none.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.tr(this.config.title || (this.page && this.page.title) || '')\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, translated.\n\t\t *\n\t\t * @return {string} The description.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The empty-state text.\n\t\t *\n\t\t * @return {string} The text.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel ? this.tr(this.config.emptyLabel) : t('nextcloud-vue', 'Nothing to open here.')\n\t\t},\n\n\t\t/**\n\t\t * The cards a reader may see and can open, with text translated and\n\t\t * the link resolved. A card that is gated away, or has no target that\n\t\t * resolves, is left out: a card that does nothing is worse than none.\n\t\t *\n\t\t * @return {Array<object>} The cards.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link\n\t\t */\n\t\tvisibleCards() {\n\t\t\tconst declared = Array.isArray(this.config.cards) ? this.config.cards : []\n\t\t\tconst runtime = this.cnManifest?.runtime ?? null\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.id && card.label)\n\t\t\t\t.filter((card) => this.passesPermission(card) && this.passesVisibleIf(card, runtime))\n\t\t\t\t.map((card) => {\n\t\t\t\t\tconst target = this.targetOf(card)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: String(card.id),\n\t\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t\t\tdescriptionId: `cn-link-card-desc-${this.uid}-${String(card.id).replace(/[^A-Za-z0-9_-]/g, '-')}`,\n\t\t\t\t\t\ticon: card.icon || '',\n\t\t\t\t\t\tcategory: card.category || '',\n\t\t\t\t\t\texternal: target.external,\n\t\t\t\t\t\tlocation: target.location,\n\t\t\t\t\t\turl: target.url,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.filter((card) => card.url !== '')\n\t\t},\n\n\t\t/**\n\t\t * The groups to draw: ungrouped cards first under no caption, then\n\t\t * one group per category in declaration order. A group without a\n\t\t * visible card is left out.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, headingId: string, cards: Array<object>}>} The groups.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tgroups() {\n\t\t\tconst declared = (this.config.categories && typeof this.config.categories === 'object') ? this.config.categories : {}\n\t\t\tconst keys = Object.keys(declared)\n\t\t\tconst loose = this.visibleCards.filter((card) => !keys.includes(card.category))\n\t\t\tconst groups = loose.length > 0 ? [{ key: '', label: '', headingId: '', cards: loose }] : []\n\t\t\tkeys.forEach((key, index) => {\n\t\t\t\tconst cards = this.visibleCards.filter((card) => card.category === key)\n\t\t\t\tif (cards.length > 0) {\n\t\t\t\t\tgroups.push({\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tlabel: this.tr(declared[key]),\n\t\t\t\t\t\theadingId: `cn-link-cards-group-${this.uid}-${index}`,\n\t\t\t\t\t\tcards,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn groups\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest string through the app's translate function.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translation, or the input unchanged.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\treturn (this.translate ?? this.cnTranslate)(value)\n\t\t},\n\n\t\t/**\n\t\t * Whether the user holds the card's `permission`. Same rule as the\n\t\t * menu: no permission declared, or no permissions known, allows.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {boolean} True when the card may render.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-cards-honour-menu-visibility-conditions\n\t\t */\n\t\tpassesPermission(card) {\n\t\t\tconst held = Array.isArray(this.cnPermissions) ? this.cnPermissions : []\n\t\t\tif (!card.permission || held.length === 0) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn held.includes(card.permission)\n\t\t},\n\n\t\t/**\n\t\t * Evaluate a card's `visibleIf` the way CnAppNav evaluates a menu\n\t\t * entry's: `appInstalled` first, then dot-path predicates against\n\t\t * the manifest's `runtime`.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {object|null} runtime `manifest.runtime`, or null.\n\t\t * @return {boolean} True when the card may render.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-cards-honour-menu-visibility-conditions\n\t\t */\n\t\tpassesVisibleIf(card, runtime) {\n\t\t\tconst condition = card.visibleIf\n\t\t\tif (!condition || typeof condition !== 'object') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (condition.appInstalled && !isAppInstalled(condition.appInstalled)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn passesContextPredicates(condition, runtime)\n\t\t},\n\n\t\t/**\n\t\t * Where a card goes. An `href` is an external URL. A `route` is a\n\t\t * route name, or a path when it starts with `/`, resolved by the\n\t\t * router so the link carries the app's base with or without\n\t\t * `/index.php`.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {{external: boolean, location: object|null, url: string}} The target; `url` is '' when it cannot be resolved.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link\n\t\t */\n\t\ttargetOf(card) {\n\t\t\tif (typeof card.route === 'string' && card.route !== '') {\n\t\t\t\tconst location = card.route.startsWith('/') ? { path: card.route } : { name: card.route }\n\t\t\t\tif (card.params && typeof card.params === 'object') {\n\t\t\t\t\tlocation.params = card.params\n\t\t\t\t}\n\t\t\t\tif (card.query && typeof card.query === 'object') {\n\t\t\t\t\tlocation.query = card.query\n\t\t\t\t}\n\t\t\t\treturn { external: false, location, url: resolveHref(location, this.$router) }\n\t\t\t}\n\t\t\tif (typeof card.href === 'string' && card.href !== '') {\n\t\t\t\treturn { external: true, location: null, url: card.href }\n\t\t\t}\n\t\t\treturn { external: false, location: null, url: '' }\n\t\t},\n\n\t\t/**\n\t\t * Route a plain click on an in-app card. A modified or middle click,\n\t\t * and every click on an external card, is left to the browser.\n\t\t *\n\t\t * @param {object} card The resolved card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link\n\t\t */\n\t\topen(card, event) {\n\t\t\tif (card.external || !card.location) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowLinkClick(event, card.location, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-link-cards-page {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(6 * var(--default-grid-baseline));\n\tpadding: calc(5 * var(--default-grid-baseline));\n}\n\n.cn-link-cards-page__title {\n\tmargin: 0 0 var(--default-grid-baseline);\n}\n\n.cn-link-cards-page__description,\n.cn-link-cards-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n\tmax-width: 70ch;\n}\n\n.cn-link-cards-page__group {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(3 * var(--default-grid-baseline));\n}\n\n.cn-link-cards-page__caption {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tfont-weight: 600;\n\tletter-spacing: 0.06em;\n\tmargin: 0;\n\ttext-transform: uppercase;\n}\n\n.cn-link-cards-page__grid {\n\tdisplay: grid;\n\tgap: calc(3 * var(--default-grid-baseline));\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-link-cards-page__cell {\n\tdisplay: flex;\n}\n\n.cn-link-cards-page__card {\n\talign-items: flex-start;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: var(--border-radius-large);\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: 1;\n\tgap: calc(3 * var(--default-grid-baseline));\n\tmin-width: 0;\n\tpadding: calc(4 * var(--default-grid-baseline));\n\ttext-decoration: none;\n}\n\n.cn-link-cards-page__card:hover {\n\tbackground: var(--color-background-hover);\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-link-cards-page__card:focus-visible {\n\tborder-color: var(--color-primary-element);\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-link-cards-page__icon {\n\tcolor: var(--color-primary-element);\n\tflex: none;\n}\n\n.cn-link-cards-page__body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: var(--default-grid-baseline);\n\tmin-width: 0;\n}\n\n.cn-link-cards-page__label {\n\tfont-weight: 600;\n\toverflow-wrap: anywhere;\n}\n\n.cn-link-cards-page__card-description {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tline-height: 1.4;\n}\n</style>\n"],"names":["CnIcon","t"],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { resolveComponent, openBlock, createElementBlock, createElementVNode, toDisplayString, createCommentVNode, Fragment, renderList, createBlock } from 'vue';
|
|
2
|
+
|
|
3
|
+
const _hoisted_1 = {
|
|
4
|
+
class: "cn-link-cards-page",
|
|
5
|
+
"data-testid": "cn-link-cards-page"
|
|
6
|
+
};
|
|
7
|
+
const _hoisted_2 = { class: "cn-link-cards-page__header" };
|
|
8
|
+
const _hoisted_3 = {
|
|
9
|
+
key: 0,
|
|
10
|
+
class: "cn-link-cards-page__title"
|
|
11
|
+
};
|
|
12
|
+
const _hoisted_4 = {
|
|
13
|
+
key: 1,
|
|
14
|
+
class: "cn-link-cards-page__description"
|
|
15
|
+
};
|
|
16
|
+
const _hoisted_5 = {
|
|
17
|
+
key: 0,
|
|
18
|
+
class: "cn-link-cards-page__empty",
|
|
19
|
+
"data-testid": "cn-link-cards-empty"
|
|
20
|
+
};
|
|
21
|
+
const _hoisted_6 = ["aria-labelledby", "aria-label", "data-group"];
|
|
22
|
+
const _hoisted_7 = ["id"];
|
|
23
|
+
const _hoisted_8 = { class: "cn-link-cards-page__grid" };
|
|
24
|
+
const _hoisted_9 = ["data-card-id", "href", "target", "rel", "aria-describedby", "onClick"];
|
|
25
|
+
const _hoisted_10 = { class: "cn-link-cards-page__body" };
|
|
26
|
+
const _hoisted_11 = { class: "cn-link-cards-page__label" };
|
|
27
|
+
const _hoisted_12 = ["id"];
|
|
28
|
+
|
|
29
|
+
function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
30
|
+
const _component_CnIcon = resolveComponent("CnIcon");
|
|
31
|
+
|
|
32
|
+
return (openBlock(), createElementBlock("div", _hoisted_1, [
|
|
33
|
+
createElementVNode("div", _hoisted_2, [
|
|
34
|
+
($options.resolvedTitle)
|
|
35
|
+
? (openBlock(), createElementBlock("h2", _hoisted_3, toDisplayString($options.resolvedTitle), 1 /* TEXT */))
|
|
36
|
+
: createCommentVNode("v-if", true),
|
|
37
|
+
($options.resolvedDescription)
|
|
38
|
+
? (openBlock(), createElementBlock("p", _hoisted_4, toDisplayString($options.resolvedDescription), 1 /* TEXT */))
|
|
39
|
+
: createCommentVNode("v-if", true)
|
|
40
|
+
]),
|
|
41
|
+
($options.groups.length === 0)
|
|
42
|
+
? (openBlock(), createElementBlock("p", _hoisted_5, toDisplayString($options.resolvedEmptyLabel), 1 /* TEXT */))
|
|
43
|
+
: createCommentVNode("v-if", true),
|
|
44
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList($options.groups, (group) => {
|
|
45
|
+
return (openBlock(), createElementBlock("section", {
|
|
46
|
+
key: group.key,
|
|
47
|
+
class: "cn-link-cards-page__group",
|
|
48
|
+
"aria-labelledby": group.label ? group.headingId : null,
|
|
49
|
+
"aria-label": group.label ? null : $options.resolvedTitle || null,
|
|
50
|
+
"data-testid": "cn-link-cards-group",
|
|
51
|
+
"data-group": group.key
|
|
52
|
+
}, [
|
|
53
|
+
(group.label)
|
|
54
|
+
? (openBlock(), createElementBlock("h3", {
|
|
55
|
+
key: 0,
|
|
56
|
+
id: group.headingId,
|
|
57
|
+
class: "cn-link-cards-page__caption"
|
|
58
|
+
}, toDisplayString(group.label), 9 /* TEXT, PROPS */, _hoisted_7))
|
|
59
|
+
: createCommentVNode("v-if", true),
|
|
60
|
+
createElementVNode("ul", _hoisted_8, [
|
|
61
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(group.cards, (card) => {
|
|
62
|
+
return (openBlock(), createElementBlock("li", {
|
|
63
|
+
key: card.id,
|
|
64
|
+
class: "cn-link-cards-page__cell"
|
|
65
|
+
}, [
|
|
66
|
+
createElementVNode("a", {
|
|
67
|
+
class: "cn-link-cards-page__card",
|
|
68
|
+
"data-testid": "cn-link-card",
|
|
69
|
+
"data-card-id": card.id,
|
|
70
|
+
href: card.url,
|
|
71
|
+
target: card.external ? '_blank' : null,
|
|
72
|
+
rel: card.external ? 'noopener noreferrer' : null,
|
|
73
|
+
"aria-describedby": card.description ? card.descriptionId : null,
|
|
74
|
+
onClick: $event => ($options.open(card, $event))
|
|
75
|
+
}, [
|
|
76
|
+
(card.icon)
|
|
77
|
+
? (openBlock(), createBlock(_component_CnIcon, {
|
|
78
|
+
key: 0,
|
|
79
|
+
name: card.icon,
|
|
80
|
+
size: 24,
|
|
81
|
+
class: "cn-link-cards-page__icon",
|
|
82
|
+
"aria-hidden": "true"
|
|
83
|
+
}, null, 8 /* PROPS */, ["name"]))
|
|
84
|
+
: createCommentVNode("v-if", true),
|
|
85
|
+
createElementVNode("span", _hoisted_10, [
|
|
86
|
+
createElementVNode("span", _hoisted_11, toDisplayString(card.label), 1 /* TEXT */),
|
|
87
|
+
(card.description)
|
|
88
|
+
? (openBlock(), createElementBlock("span", {
|
|
89
|
+
key: 0,
|
|
90
|
+
id: card.descriptionId,
|
|
91
|
+
class: "cn-link-cards-page__card-description"
|
|
92
|
+
}, toDisplayString(card.description), 9 /* TEXT, PROPS */, _hoisted_12))
|
|
93
|
+
: createCommentVNode("v-if", true)
|
|
94
|
+
])
|
|
95
|
+
], 8 /* PROPS */, _hoisted_9)
|
|
96
|
+
]))
|
|
97
|
+
}), 128 /* KEYED_FRAGMENT */))
|
|
98
|
+
])
|
|
99
|
+
], 8 /* PROPS */, _hoisted_6))
|
|
100
|
+
}), 128 /* KEYED_FRAGMENT */))
|
|
101
|
+
]))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { render };
|
|
105
|
+
//# sourceMappingURL=CnLinkCardsPage.vue3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnLinkCardsPage.vue3.js","sources":["../../../../src/components/CnLinkCardsPage/CnLinkCardsPage.vue?vue&type=template&id=2f9a546b&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-link-cards-page\" data-testid=\"cn-link-cards-page\">\n\t\t<div class=\"cn-link-cards-page__header\">\n\t\t\t<h2 v-if=\"resolvedTitle\" class=\"cn-link-cards-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-link-cards-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<p\n\t\t\tv-if=\"groups.length === 0\"\n\t\t\tclass=\"cn-link-cards-page__empty\"\n\t\t\tdata-testid=\"cn-link-cards-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<section\n\t\t\tv-for=\"group in groups\"\n\t\t\t:key=\"group.key\"\n\t\t\tclass=\"cn-link-cards-page__group\"\n\t\t\t:aria-labelledby=\"group.label ? group.headingId : null\"\n\t\t\t:aria-label=\"group.label ? null : resolvedTitle || null\"\n\t\t\tdata-testid=\"cn-link-cards-group\"\n\t\t\t:data-group=\"group.key\">\n\t\t\t<h3\n\t\t\t\tv-if=\"group.label\"\n\t\t\t\t:id=\"group.headingId\"\n\t\t\t\tclass=\"cn-link-cards-page__caption\">\n\t\t\t\t{{ group.label }}\n\t\t\t</h3>\n\t\t\t<ul class=\"cn-link-cards-page__grid\">\n\t\t\t\t<li v-for=\"card in group.cards\" :key=\"card.id\" class=\"cn-link-cards-page__cell\">\n\t\t\t\t\t<!-- A real <a href>, so the card can be middle-clicked, copied\n\t\t\t\t\t and announced as a link. A plain click on an in-app card\n\t\t\t\t\t is routed, which keeps the page from reloading. -->\n\t\t\t\t\t<a\n\t\t\t\t\t\tclass=\"cn-link-cards-page__card\"\n\t\t\t\t\t\tdata-testid=\"cn-link-card\"\n\t\t\t\t\t\t:data-card-id=\"card.id\"\n\t\t\t\t\t\t:href=\"card.url\"\n\t\t\t\t\t\t:target=\"card.external ? '_blank' : null\"\n\t\t\t\t\t\t:rel=\"card.external ? 'noopener noreferrer' : null\"\n\t\t\t\t\t\t:aria-describedby=\"card.description ? card.descriptionId : null\"\n\t\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\tv-if=\"card.icon\"\n\t\t\t\t\t\t\t:name=\"card.icon\"\n\t\t\t\t\t\t\t:size=\"24\"\n\t\t\t\t\t\t\tclass=\"cn-link-cards-page__icon\"\n\t\t\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t\t\t<span class=\"cn-link-cards-page__body\">\n\t\t\t\t\t\t\t<span class=\"cn-link-cards-page__label\">{{ card.label }}</span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"card.description\"\n\t\t\t\t\t\t\t\t:id=\"card.descriptionId\"\n\t\t\t\t\t\t\t\tclass=\"cn-link-cards-page__card-description\">\n\t\t\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</a>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\nimport { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'\nimport { passesContextPredicates } from '../../utils/visibleIfContext.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\nlet linkCardsPageUid = 0\n\n/**\n * CnLinkCardsPage: the `type: \"links\"` page. Link cards, grouped under captions.\n *\n * One page that opens every page a short menu leaves out. A card has a\n * label, a one-line description, an optional icon and a target. Groups come\n * from `categories`, in the order they are declared; a card names its group\n * with `category`.\n *\n * ```json\n * { \"id\": \"Modules\", \"route\": \"/modules\", \"type\": \"links\", \"title\": \"Modules and more\",\n * \"config\": {\n * \"description\": \"Every page that is not in the daily menu.\",\n * \"categories\": { \"sales\": \"Sales\", \"marketing\": \"Marketing\" },\n * \"cards\": [\n * { \"id\": \"Leads\", \"label\": \"Leads\", \"description\": \"Every deal you are working on.\",\n * \"icon\": \"CashMultiple\", \"category\": \"sales\", \"route\": \"Leads\" }\n * ]\n * } }\n * ```\n *\n * Every string goes through the app's translate function. A card is hidden\n * by the same `visibleIf` and `permission` a menu entry takes, and a group\n * with no visible card is not drawn.\n */\nexport default {\n\tname: 'CnLinkCardsPage',\n\n\tcomponents: { CnIcon },\n\n\tinject: {\n\t\t/** The app's translate function, provided by CnAppRoot. Identity outside one. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The effective manifest, for `visibleIf` predicates against `runtime`. */\n\t\tcnManifest: { default: null },\n\t\t/** The permissions the user holds. Empty means the app did not say, and allows. */\n\t\tcnPermissions: { default: () => [] },\n\t},\n\n\tprops: {\n\t\t/** The whole manifest page, for a host that mounts this component itself. CnPageRenderer passes the flattened props below instead. */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The cards: `{ id, label, description?, icon?, category?, route?, params?, query?, href?, visibleIf?, permission? }`.\n\t\t * `route` is a route name, or a path when it starts with `/`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Group key to caption. Sets the order of the groups.\n\t\t *\n\t\t * @type {Record<string, string>|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The heading. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The lead paragraph under the heading. */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown when no card is visible. */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\tlinkCardsPageUid += 1\n\t\treturn {\n\t\t\tuid: linkCardsPageUid,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The config, from the flattened props or from `page.config`. Props win.\n\t\t *\n\t\t * @return {object} The config.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of ['cards', 'categories', 'description', 'title', 'emptyLabel']) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The heading, translated.\n\t\t *\n\t\t * @return {string} The title, or '' when the page declares none.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.tr(this.config.title || (this.page && this.page.title) || '')\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, translated.\n\t\t *\n\t\t * @return {string} The description.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The empty-state text.\n\t\t *\n\t\t * @return {string} The text.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel ? this.tr(this.config.emptyLabel) : t('nextcloud-vue', 'Nothing to open here.')\n\t\t},\n\n\t\t/**\n\t\t * The cards a reader may see and can open, with text translated and\n\t\t * the link resolved. A card that is gated away, or has no target that\n\t\t * resolves, is left out: a card that does nothing is worse than none.\n\t\t *\n\t\t * @return {Array<object>} The cards.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link\n\t\t */\n\t\tvisibleCards() {\n\t\t\tconst declared = Array.isArray(this.config.cards) ? this.config.cards : []\n\t\t\tconst runtime = this.cnManifest?.runtime ?? null\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.id && card.label)\n\t\t\t\t.filter((card) => this.passesPermission(card) && this.passesVisibleIf(card, runtime))\n\t\t\t\t.map((card) => {\n\t\t\t\t\tconst target = this.targetOf(card)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: String(card.id),\n\t\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t\t\tdescriptionId: `cn-link-card-desc-${this.uid}-${String(card.id).replace(/[^A-Za-z0-9_-]/g, '-')}`,\n\t\t\t\t\t\ticon: card.icon || '',\n\t\t\t\t\t\tcategory: card.category || '',\n\t\t\t\t\t\texternal: target.external,\n\t\t\t\t\t\tlocation: target.location,\n\t\t\t\t\t\turl: target.url,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.filter((card) => card.url !== '')\n\t\t},\n\n\t\t/**\n\t\t * The groups to draw: ungrouped cards first under no caption, then\n\t\t * one group per category in declaration order. A group without a\n\t\t * visible card is left out.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, headingId: string, cards: Array<object>}>} The groups.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tgroups() {\n\t\t\tconst declared = (this.config.categories && typeof this.config.categories === 'object') ? this.config.categories : {}\n\t\t\tconst keys = Object.keys(declared)\n\t\t\tconst loose = this.visibleCards.filter((card) => !keys.includes(card.category))\n\t\t\tconst groups = loose.length > 0 ? [{ key: '', label: '', headingId: '', cards: loose }] : []\n\t\t\tkeys.forEach((key, index) => {\n\t\t\t\tconst cards = this.visibleCards.filter((card) => card.category === key)\n\t\t\t\tif (cards.length > 0) {\n\t\t\t\t\tgroups.push({\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tlabel: this.tr(declared[key]),\n\t\t\t\t\t\theadingId: `cn-link-cards-group-${this.uid}-${index}`,\n\t\t\t\t\t\tcards,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn groups\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest string through the app's translate function.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translation, or the input unchanged.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\treturn (this.translate ?? this.cnTranslate)(value)\n\t\t},\n\n\t\t/**\n\t\t * Whether the user holds the card's `permission`. Same rule as the\n\t\t * menu: no permission declared, or no permissions known, allows.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {boolean} True when the card may render.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-cards-honour-menu-visibility-conditions\n\t\t */\n\t\tpassesPermission(card) {\n\t\t\tconst held = Array.isArray(this.cnPermissions) ? this.cnPermissions : []\n\t\t\tif (!card.permission || held.length === 0) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn held.includes(card.permission)\n\t\t},\n\n\t\t/**\n\t\t * Evaluate a card's `visibleIf` the way CnAppNav evaluates a menu\n\t\t * entry's: `appInstalled` first, then dot-path predicates against\n\t\t * the manifest's `runtime`.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {object|null} runtime `manifest.runtime`, or null.\n\t\t * @return {boolean} True when the card may render.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-cards-honour-menu-visibility-conditions\n\t\t */\n\t\tpassesVisibleIf(card, runtime) {\n\t\t\tconst condition = card.visibleIf\n\t\t\tif (!condition || typeof condition !== 'object') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (condition.appInstalled && !isAppInstalled(condition.appInstalled)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn passesContextPredicates(condition, runtime)\n\t\t},\n\n\t\t/**\n\t\t * Where a card goes. An `href` is an external URL. A `route` is a\n\t\t * route name, or a path when it starts with `/`, resolved by the\n\t\t * router so the link carries the app's base with or without\n\t\t * `/index.php`.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {{external: boolean, location: object|null, url: string}} The target; `url` is '' when it cannot be resolved.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link\n\t\t */\n\t\ttargetOf(card) {\n\t\t\tif (typeof card.route === 'string' && card.route !== '') {\n\t\t\t\tconst location = card.route.startsWith('/') ? { path: card.route } : { name: card.route }\n\t\t\t\tif (card.params && typeof card.params === 'object') {\n\t\t\t\t\tlocation.params = card.params\n\t\t\t\t}\n\t\t\t\tif (card.query && typeof card.query === 'object') {\n\t\t\t\t\tlocation.query = card.query\n\t\t\t\t}\n\t\t\t\treturn { external: false, location, url: resolveHref(location, this.$router) }\n\t\t\t}\n\t\t\tif (typeof card.href === 'string' && card.href !== '') {\n\t\t\t\treturn { external: true, location: null, url: card.href }\n\t\t\t}\n\t\t\treturn { external: false, location: null, url: '' }\n\t\t},\n\n\t\t/**\n\t\t * Route a plain click on an in-app card. A modified or middle click,\n\t\t * and every click on an external card, is left to the browser.\n\t\t *\n\t\t * @param {object} card The resolved card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link\n\t\t */\n\t\topen(card, event) {\n\t\t\tif (card.external || !card.location) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowLinkClick(event, card.location, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-link-cards-page {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(6 * var(--default-grid-baseline));\n\tpadding: calc(5 * var(--default-grid-baseline));\n}\n\n.cn-link-cards-page__title {\n\tmargin: 0 0 var(--default-grid-baseline);\n}\n\n.cn-link-cards-page__description,\n.cn-link-cards-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n\tmax-width: 70ch;\n}\n\n.cn-link-cards-page__group {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(3 * var(--default-grid-baseline));\n}\n\n.cn-link-cards-page__caption {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tfont-weight: 600;\n\tletter-spacing: 0.06em;\n\tmargin: 0;\n\ttext-transform: uppercase;\n}\n\n.cn-link-cards-page__grid {\n\tdisplay: grid;\n\tgap: calc(3 * var(--default-grid-baseline));\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-link-cards-page__cell {\n\tdisplay: flex;\n}\n\n.cn-link-cards-page__card {\n\talign-items: flex-start;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: var(--border-radius-large);\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: 1;\n\tgap: calc(3 * var(--default-grid-baseline));\n\tmin-width: 0;\n\tpadding: calc(4 * var(--default-grid-baseline));\n\ttext-decoration: none;\n}\n\n.cn-link-cards-page__card:hover {\n\tbackground: var(--color-background-hover);\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-link-cards-page__card:focus-visible {\n\tborder-color: var(--color-primary-element);\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-link-cards-page__icon {\n\tcolor: var(--color-primary-element);\n\tflex: none;\n}\n\n.cn-link-cards-page__body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: var(--default-grid-baseline);\n\tmin-width: 0;\n}\n\n.cn-link-cards-page__label {\n\tfont-weight: 600;\n\toverflow-wrap: anywhere;\n}\n\n.cn-link-cards-page__card-description {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tline-height: 1.4;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_renderList","_createElementVNode","_createBlock"],"mappings":"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"}
|
|
@@ -57,6 +57,7 @@ const defaultPageTypes = {
|
|
|
57
57
|
map: defineAsyncComponent(() => import('../CnMapPage/CnMapPage.vue.js').then((m) => m.default)),
|
|
58
58
|
roadmap: defineAsyncComponent(() => import('../CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue.js').then((m) => m.default)),
|
|
59
59
|
reports: defineAsyncComponent(() => import('../CnReportsPage/CnReportsPage.vue.js').then((m) => m.default)),
|
|
60
|
+
links: defineAsyncComponent(() => import('../CnLinkCardsPage/CnLinkCardsPage.vue.js').then((m) => m.default)),
|
|
60
61
|
store: defineAsyncComponent(() => import('../CnStorePage/CnStorePage.vue.js').then((m) => m.default)),
|
|
61
62
|
search: defineAsyncComponent(() => import('../CnSearchPage/CnSearchPage.vue.js').then((m) => m.default)),
|
|
62
63
|
wiki: defineAsyncComponent(() => import('../CnWikiPage/CnWikiPage.vue.js').then((m) => m.default)),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pageTypes.js","sources":["../../../../src/components/CnPageRenderer/pageTypes.js"],"sourcesContent":["import { defineAsyncComponent } from 'vue'\n\n/**\n * Default mapping from manifest `pages[].type` value to the Vue\n * component the renderer mounts.\n *\n * The library ships built-in types here; consumers and downstream\n * library extensions can add their own by passing a merged map to\n * `CnAppRoot` (or `CnPageRenderer`) via the `pageTypes` prop.\n *\n * Each entry uses `defineAsyncComponent` with an explicit\n * `.then(m => m.default)` unwrap. The unwrap is load-bearing: rollup's\n * `inlineDynamicImports: true` flattens every `import()` call into a\n * `Promise.resolve().then(() => namespace)` where `namespace` is a\n * frozen `Object.freeze({__proto__: null, default: <component>})`.\n * Vue 2's async-component resolution against that frozen wrapper trips\n * `Cannot add property _Ctor, object is not extensible` when downstream\n * `Vue.extend()` (e.g. via Vue Router or `<component :is>`) tries to\n * attach its internal `_Ctor` cache. Pre-unwrapping the `default`\n * property yields the raw, extensible component options object —\n * which Vue can then mutate freely.\n *\n * Keeping `defineAsyncComponent` (rather than collapsing to static\n * imports) preserves the test-environment hack of NOT loading\n * `CnDashboardPage` → `CnDashboardGrid` → `gridstack` at module-load\n * time; gridstack ships ESM that Jest's default transform refuses\n * unless explicitly allowlisted. Async imports keep that load\n * deferred to actual render.\n *\n * The special `custom` type is NOT registered here — CnPageRenderer\n * handles it inline, resolving `page.component` against the\n * customComponents registry rather than this map.\n *\n * @example Extending with an app-specific page type\n *\n * import { defaultPageTypes } from '@conduction/nextcloud-vue'\n * import MyReportPage from './views/MyReportPage.vue'\n *\n * const pageTypes = { ...defaultPageTypes, report: MyReportPage }\n *\n * <CnAppRoot :manifest=\"manifest\" app-id=\"myapp\" :page-types=\"pageTypes\" />\n *\n * @example Adding a built-in page type to the library\n *\n * Add a new entry to this map and export the component from the\n * `src/components/index.js` barrel. No change to CnPageRenderer.vue.\n */\nexport const defaultPageTypes = {\n\tindex: defineAsyncComponent(() => import('../CnIndexPage/CnIndexPage.vue').then((m) => m.default)),\n\tdetail: defineAsyncComponent(() => import('../CnDetailPage/CnDetailPage.vue').then((m) => m.default)),\n\tdashboard: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue').then((m) => m.default)),\n\tlogs: defineAsyncComponent(() => import('../CnLogsPage/CnLogsPage.vue').then((m) => m.default)),\n\tsettings: defineAsyncComponent(() => import('../CnSettingsPage/CnSettingsPage.vue').then((m) => m.default)),\n\tchat: defineAsyncComponent(() => import('../CnChatPage/CnChatPage.vue').then((m) => m.default)),\n\tfiles: defineAsyncComponent(() => import('../CnFilesPage/CnFilesPage.vue').then((m) => m.default)),\n\tform: defineAsyncComponent(() => import('../CnFormPage/CnFormPage.vue').then((m) => m.default)),\n\tmap: defineAsyncComponent(() => import('../CnMapPage/CnMapPage.vue').then((m) => m.default)),\n\troadmap: defineAsyncComponent(() => import('../CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue').then((m) => m.default)),\n\treports: defineAsyncComponent(() => import('../CnReportsPage/CnReportsPage.vue').then((m) => m.default)),\n\tstore: defineAsyncComponent(() => import('../CnStorePage/CnStorePage.vue').then((m) => m.default)),\n\tsearch: defineAsyncComponent(() => import('../CnSearchPage/CnSearchPage.vue').then((m) => m.default)),\n\twiki: defineAsyncComponent(() => import('../CnWikiPage/CnWikiPage.vue').then((m) => m.default)),\n\t// A flow list USED TO need its own page type. A flow lives in\n\t// OpenRegister's native flow store, not a register/schema pair, so the\n\t// object-backed index had nothing to bind to — and every app adopting flows\n\t// copied ~270 lines of identical wrapper differing only in an app-id\n\t// string, which is how three apps ended up with the same dead `@rowClick`\n\t// listener (CnIndexPage emits `row-click`).\n\t//\n\t// Named index sources removed that constraint: `type: \"index\"` with\n\t// `config.entitySource: \"flows\"` lists them, so only the EDITOR still needs a\n\t// page type of its own.\n\t//\n\t// The flow EDITOR. `flow` opens one flow; the LIST is now an ordinary\n\t// `type: \"index\"` with `config.entitySource: \"flows\"`, so a flow surface needs no\n\t// custom page and no page type of its own for the list half.\n\tflow: defineAsyncComponent(() => import('../CnFlowsPage/CnFlowEditorPage.vue').then((m) => m.default)),\n\n\t// DEPRECATED, kept so the fleet can migrate without a flag day. `flows`\n\t// predates named index sources and `flow-detail` is the old name for\n\t// `flow`. Both still resolve; remove them once no manifest names them.\n\tflows: defineAsyncComponent(() => import('../CnFlowsPage/CnFlowsPage.vue').then((m) => m.default)),\n\t'flow-detail': defineAsyncComponent(() => import('../CnFlowsPage/CnFlowEditorPage.vue').then((m) => m.default)),\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pageTypes.js","sources":["../../../../src/components/CnPageRenderer/pageTypes.js"],"sourcesContent":["import { defineAsyncComponent } from 'vue'\n\n/**\n * Default mapping from manifest `pages[].type` value to the Vue\n * component the renderer mounts.\n *\n * The library ships built-in types here; consumers and downstream\n * library extensions can add their own by passing a merged map to\n * `CnAppRoot` (or `CnPageRenderer`) via the `pageTypes` prop.\n *\n * Each entry uses `defineAsyncComponent` with an explicit\n * `.then(m => m.default)` unwrap. The unwrap is load-bearing: rollup's\n * `inlineDynamicImports: true` flattens every `import()` call into a\n * `Promise.resolve().then(() => namespace)` where `namespace` is a\n * frozen `Object.freeze({__proto__: null, default: <component>})`.\n * Vue 2's async-component resolution against that frozen wrapper trips\n * `Cannot add property _Ctor, object is not extensible` when downstream\n * `Vue.extend()` (e.g. via Vue Router or `<component :is>`) tries to\n * attach its internal `_Ctor` cache. Pre-unwrapping the `default`\n * property yields the raw, extensible component options object —\n * which Vue can then mutate freely.\n *\n * Keeping `defineAsyncComponent` (rather than collapsing to static\n * imports) preserves the test-environment hack of NOT loading\n * `CnDashboardPage` → `CnDashboardGrid` → `gridstack` at module-load\n * time; gridstack ships ESM that Jest's default transform refuses\n * unless explicitly allowlisted. Async imports keep that load\n * deferred to actual render.\n *\n * The special `custom` type is NOT registered here — CnPageRenderer\n * handles it inline, resolving `page.component` against the\n * customComponents registry rather than this map.\n *\n * @example Extending with an app-specific page type\n *\n * import { defaultPageTypes } from '@conduction/nextcloud-vue'\n * import MyReportPage from './views/MyReportPage.vue'\n *\n * const pageTypes = { ...defaultPageTypes, report: MyReportPage }\n *\n * <CnAppRoot :manifest=\"manifest\" app-id=\"myapp\" :page-types=\"pageTypes\" />\n *\n * @example Adding a built-in page type to the library\n *\n * Add a new entry to this map and export the component from the\n * `src/components/index.js` barrel. No change to CnPageRenderer.vue.\n */\nexport const defaultPageTypes = {\n\tindex: defineAsyncComponent(() => import('../CnIndexPage/CnIndexPage.vue').then((m) => m.default)),\n\tdetail: defineAsyncComponent(() => import('../CnDetailPage/CnDetailPage.vue').then((m) => m.default)),\n\tdashboard: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue').then((m) => m.default)),\n\tlogs: defineAsyncComponent(() => import('../CnLogsPage/CnLogsPage.vue').then((m) => m.default)),\n\tsettings: defineAsyncComponent(() => import('../CnSettingsPage/CnSettingsPage.vue').then((m) => m.default)),\n\tchat: defineAsyncComponent(() => import('../CnChatPage/CnChatPage.vue').then((m) => m.default)),\n\tfiles: defineAsyncComponent(() => import('../CnFilesPage/CnFilesPage.vue').then((m) => m.default)),\n\tform: defineAsyncComponent(() => import('../CnFormPage/CnFormPage.vue').then((m) => m.default)),\n\tmap: defineAsyncComponent(() => import('../CnMapPage/CnMapPage.vue').then((m) => m.default)),\n\troadmap: defineAsyncComponent(() => import('../CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue').then((m) => m.default)),\n\treports: defineAsyncComponent(() => import('../CnReportsPage/CnReportsPage.vue').then((m) => m.default)),\n\tlinks: defineAsyncComponent(() => import('../CnLinkCardsPage/CnLinkCardsPage.vue').then((m) => m.default)),\n\tstore: defineAsyncComponent(() => import('../CnStorePage/CnStorePage.vue').then((m) => m.default)),\n\tsearch: defineAsyncComponent(() => import('../CnSearchPage/CnSearchPage.vue').then((m) => m.default)),\n\twiki: defineAsyncComponent(() => import('../CnWikiPage/CnWikiPage.vue').then((m) => m.default)),\n\t// A flow list USED TO need its own page type. A flow lives in\n\t// OpenRegister's native flow store, not a register/schema pair, so the\n\t// object-backed index had nothing to bind to — and every app adopting flows\n\t// copied ~270 lines of identical wrapper differing only in an app-id\n\t// string, which is how three apps ended up with the same dead `@rowClick`\n\t// listener (CnIndexPage emits `row-click`).\n\t//\n\t// Named index sources removed that constraint: `type: \"index\"` with\n\t// `config.entitySource: \"flows\"` lists them, so only the EDITOR still needs a\n\t// page type of its own.\n\t//\n\t// The flow EDITOR. `flow` opens one flow; the LIST is now an ordinary\n\t// `type: \"index\"` with `config.entitySource: \"flows\"`, so a flow surface needs no\n\t// custom page and no page type of its own for the list half.\n\tflow: defineAsyncComponent(() => import('../CnFlowsPage/CnFlowEditorPage.vue').then((m) => m.default)),\n\n\t// DEPRECATED, kept so the fleet can migrate without a flag day. `flows`\n\t// predates named index sources and `flow-detail` is the old name for\n\t// `flow`. Both still resolve; remove them once no manifest names them.\n\tflows: defineAsyncComponent(() => import('../CnFlowsPage/CnFlowsPage.vue').then((m) => m.default)),\n\t'flow-detail': defineAsyncComponent(() => import('../CnFlowsPage/CnFlowEditorPage.vue').then((m) => m.default)),\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/index.js
CHANGED
|
@@ -312,6 +312,12 @@ export { default as CnStackedBarWidget } from './components/CnStackedBarWidget/C
|
|
|
312
312
|
export { default as CnStackedBarWidgetForm } from './components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js';
|
|
313
313
|
export { default as CnWeekStripWidget } from './components/CnWeekStripWidget/CnWeekStripWidget.vue.js';
|
|
314
314
|
export { default as CnWeekStripWidgetForm } from './components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js';
|
|
315
|
+
export { default as CnFormPage } from './components/CnFormPage/CnFormPage.vue.js';
|
|
316
|
+
export { default as CnLinkCardsPage } from './components/CnLinkCardsPage/CnLinkCardsPage.vue.js';
|
|
317
|
+
export { default as CnMapPage } from './components/CnMapPage/CnMapPage.vue.js';
|
|
318
|
+
export { default as CnReportsPage } from './components/CnReportsPage/CnReportsPage.vue.js';
|
|
319
|
+
export { default as CnStorePage } from './components/CnStorePage/CnStorePage.vue.js';
|
|
320
|
+
export { default as CnWikiPage } from './components/CnWikiPage/CnWikiPage.vue.js';
|
|
315
321
|
export { default as CnAiAgentPicker } from './components/CnAiCompanion/CnAiAgentPicker.vue.js';
|
|
316
322
|
export { default as CnAiChatPanel } from './components/CnAiCompanion/CnAiChatPanel.vue.js';
|
|
317
323
|
export { default as CnAiCompanion } from './components/CnAiCompanion/CnAiCompanion.vue.js';
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/esm/l10n/en.json.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/esm/l10n/nl.json.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|