@conduction/nextcloud-vue 2.112.0 → 2.113.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/CnActionButtons/CnActionButtons.vue.js +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +4 -6
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- 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 +18 -3
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +20 -5
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +56 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +29 -12
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +3 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +152 -181
- package/dist/esm/utils/actionLink.js +104 -1
- package/dist/esm/utils/actionLink.js.map +1 -1
- 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 +241 -50
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +152 -181
- package/package.json +1 -1
- package/src/components/CnActionButtons/CnActionButtons.vue +4 -6
- package/src/components/CnChoiceCards/CnChoiceCards.vue +0 -3
- package/src/components/CnDashboardPage/CnDashboardPage.vue +20 -5
- package/src/components/CnDetailPage/CnDetailPage.vue +20 -5
- package/src/components/CnIndexPage/CnIndexPage.vue +59 -14
- package/src/components/CnReportsPage/CnReportsPage.vue +31 -12
- package/src/css/badge.css +15 -13
- package/src/css/board.css +0 -1
- package/src/css/look-board-index.css +3 -17
- package/src/css/look-board.css +2 -16
- package/src/schemas/app-manifest-v2.schema.json +51 -9
- package/src/utils/actionLink.js +103 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.113.0",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -329,12 +329,10 @@ export default {
|
|
|
329
329
|
* browser keeps middle-click, "open in new tab" and the link semantics
|
|
330
330
|
* assistive tech announces.
|
|
331
331
|
*
|
|
332
|
-
* `href` is
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
* `type: "navigate"` with an external `target`, which auto-derives the
|
|
337
|
-
* anchor/`window.open` behaviour instead.
|
|
332
|
+
* `href` is used as given, so it must be a finished URL. A manifest
|
|
333
|
+
* `headerActions[]` entry may carry `href` too: the page (CnDetailPage,
|
|
334
|
+
* CnDashboardPage) resolves an instance path through `generateUrl()`
|
|
335
|
+
* and sets `target` before the entry reaches this component.
|
|
338
336
|
*
|
|
339
337
|
* @type {Array<object>}
|
|
340
338
|
*/
|
|
@@ -522,7 +522,6 @@ export default {
|
|
|
522
522
|
still legible (DqVolgendeStap), not faded. */
|
|
523
523
|
.cn-choice-cards__row--disabled,
|
|
524
524
|
.cn-choice-cards__row--disabled:hover {
|
|
525
|
-
--nldesign-color-on-surface: var(--color-text-maxcontrast);
|
|
526
525
|
border-color: var(--color-border);
|
|
527
526
|
background: var(--color-background-hover);
|
|
528
527
|
color: var(--color-text-maxcontrast);
|
|
@@ -532,7 +531,6 @@ export default {
|
|
|
532
531
|
/* The title takes the muted ink itself: a theme that colours every label
|
|
533
532
|
with `!important` (thematiq) would otherwise hand it the label colour. */
|
|
534
533
|
.cn-choice-cards__row--disabled .cn-choice-cards__row-title {
|
|
535
|
-
--nldesign-color-on-surface: var(--color-text-maxcontrast);
|
|
536
534
|
color: var(--color-text-maxcontrast);
|
|
537
535
|
}
|
|
538
536
|
|
|
@@ -600,7 +598,6 @@ export default {
|
|
|
600
598
|
with `!important` (thematiq) reaches a span that only inherits. */
|
|
601
599
|
.cn-choice-cards__row-description,
|
|
602
600
|
.cn-choice-cards__reason {
|
|
603
|
-
--nldesign-color-on-surface: var(--color-text-maxcontrast);
|
|
604
601
|
font-size: 13px;
|
|
605
602
|
font-weight: 400;
|
|
606
603
|
color: var(--color-text-maxcontrast);
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
<template v-if="!isBoardLook">
|
|
97
97
|
<CnActionButtons
|
|
98
98
|
v-if="headerActions && headerActions.length"
|
|
99
|
-
:actions="
|
|
99
|
+
:actions="linkedHeaderActions"
|
|
100
100
|
data-testid="cn-dashboard-page-header-actions" />
|
|
101
101
|
<!-- @slot header-actions Inline buttons rendered in the dashboard header next to the edit toggle. Used by every existing consumer (decidesk, launchpad, opencatalogi, pipelinq, procest). -->
|
|
102
102
|
<slot name="header-actions" />
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
<CnBuildiqEditButton />
|
|
170
170
|
<CnActionButtons
|
|
171
171
|
v-if="headerActions && headerActions.length"
|
|
172
|
-
:actions="
|
|
172
|
+
:actions="linkedHeaderActions"
|
|
173
173
|
data-testid="cn-dashboard-page-header-actions" />
|
|
174
174
|
<slot name="header-actions" />
|
|
175
175
|
<slot name="actions" />
|
|
@@ -1042,6 +1042,7 @@ import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry
|
|
|
1042
1042
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
1043
1043
|
import { pageViews } from '../../mixins/pageViews.js'
|
|
1044
1044
|
import { mergeUserLayout, resolveUserLayoutApi } from '../../store/plugins/dashboardLayouts.js'
|
|
1045
|
+
import { headerActionAsLink } from '../../utils/actionLink.js'
|
|
1045
1046
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
1046
1047
|
import { contentHeaderLink } from '../../utils/contentHeaderLink.js'
|
|
1047
1048
|
import { pickDeclaredProps } from '../../utils/declaredProps.js'
|
|
@@ -1693,9 +1694,12 @@ export default {
|
|
|
1693
1694
|
* in the dashboard header via CnActionButtons — `open-form` (schema
|
|
1694
1695
|
* create dialog), `api-call` (POST/PUT + toast + refresh), `toggle`
|
|
1695
1696
|
* (two-way state button), `navigate` / `open-modal` / `refresh`, each
|
|
1696
|
-
* with an optional `visibleWhen` predicate.
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1697
|
+
* with an optional `visibleWhen` predicate. An entry with `href` is a
|
|
1698
|
+
* link button, never dispatched: an absolute `http(s)`, `mailto:`,
|
|
1699
|
+
* `tel:` or `//host/...` URL as written, an instance path (leading `/`) through
|
|
1700
|
+
* `generateUrl()`, in a new tab with `target: '_blank'`. Empty (the
|
|
1701
|
+
* default) renders nothing; the `#header-actions` slot still works
|
|
1702
|
+
* alongside it for bespoke buttons.
|
|
1699
1703
|
*
|
|
1700
1704
|
* @type {Array<object>}
|
|
1701
1705
|
*/
|
|
@@ -2102,6 +2106,17 @@ export default {
|
|
|
2102
2106
|
return typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key
|
|
2103
2107
|
},
|
|
2104
2108
|
|
|
2109
|
+
/**
|
|
2110
|
+
* The header actions handed to CnActionButtons, an entry with `href`
|
|
2111
|
+
* in its link form (resolved URL, anchor target, no handler).
|
|
2112
|
+
*
|
|
2113
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
2114
|
+
* @return {Array<object>}
|
|
2115
|
+
*/
|
|
2116
|
+
linkedHeaderActions() {
|
|
2117
|
+
return (this.headerActions || []).map(headerActionAsLink)
|
|
2118
|
+
},
|
|
2119
|
+
|
|
2105
2120
|
/**
|
|
2106
2121
|
* The greeting for the time of day, with the reader's first (or
|
|
2107
2122
|
* full) name when known, or '' when `greeting` is off.
|
|
@@ -1257,6 +1257,7 @@ import { createRefLabelResolver } from '../../composables/useRefLabels.js'
|
|
|
1257
1257
|
import { gridLayout } from '../../mixins/gridLayout.js'
|
|
1258
1258
|
import { pageViews } from '../../mixins/pageViews.js'
|
|
1259
1259
|
import { useObjectStore } from '../../store/index.js'
|
|
1260
|
+
import { headerActionAsLink } from '../../utils/actionLink.js'
|
|
1260
1261
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
1261
1262
|
import { compactLayoutRows } from '../../utils/dashboardPlacement.js'
|
|
1262
1263
|
import { defaultDetailGrid } from '../../utils/defaultDetailGrid.js'
|
|
@@ -2219,6 +2220,11 @@ export default {
|
|
|
2219
2220
|
* `@objectId` / `@object.<field>` token + local-predicate resolution.
|
|
2220
2221
|
* Empty (the default) renders nothing.
|
|
2221
2222
|
*
|
|
2223
|
+
* An entry with `href` is a link, never dispatched: an absolute
|
|
2224
|
+
* `http(s)`, `mailto:`, `tel:` or `//host/...` URL as written, or an instance path
|
|
2225
|
+
* (leading `/`) through `generateUrl()`, opening in place unless
|
|
2226
|
+
* `target` is `'_blank'`.
|
|
2227
|
+
*
|
|
2222
2228
|
* The menu, not a row of buttons: that is what the manifest schema has
|
|
2223
2229
|
* always said this key is, and what CnIndexPage already does with it.
|
|
2224
2230
|
* The two surfaces had drifted, and on a dossiq case — twelve actions —
|
|
@@ -3941,6 +3947,7 @@ export default {
|
|
|
3941
3947
|
* in. Edit goes LAST so it reads after the app's own actions in the
|
|
3942
3948
|
* Actions menu — the page's affordance yields to the app's.
|
|
3943
3949
|
*
|
|
3950
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
3944
3951
|
* @return {Array<object>} The action descriptors.
|
|
3945
3952
|
*/
|
|
3946
3953
|
effectiveHeaderActions() {
|
|
@@ -3949,12 +3956,16 @@ export default {
|
|
|
3949
3956
|
// written out in place) join the list CnActionButtons dispatches,
|
|
3950
3957
|
// so they get the same dialogs, toasts and `visibleWhen` gating as
|
|
3951
3958
|
// any header action. An id that is already declared is not added
|
|
3952
|
-
// twice. With nothing pinned inline
|
|
3959
|
+
// twice. With nothing pinned inline and no `href` entry this is
|
|
3960
|
+
// `headerActions` itself.
|
|
3953
3961
|
const known = new Set(authored.map((action) => action && action.id))
|
|
3954
3962
|
const inline = [this.stagePrimary, ...this.pinnedQuickActions]
|
|
3955
3963
|
.filter((pinned) => pinned && pinned.inline && !known.has(pinned.id))
|
|
3956
3964
|
.map((pinned) => pinned.inline)
|
|
3957
|
-
const
|
|
3965
|
+
const combined = inline.length > 0 ? [...authored, ...inline] : authored
|
|
3966
|
+
// An entry (or child) with `href` becomes a link CnActionButtons renders as an anchor.
|
|
3967
|
+
const linked = combined.map(headerActionAsLink)
|
|
3968
|
+
const declared = linked.some((action, index) => action !== combined[index]) ? linked : combined
|
|
3958
3969
|
if (!(this.foldsEditIntoActions && this.canEditRecord)) {
|
|
3959
3970
|
return declared
|
|
3960
3971
|
}
|
|
@@ -5198,12 +5209,13 @@ export default {
|
|
|
5198
5209
|
*
|
|
5199
5210
|
* A stage's action is dispatched the way a header action is, through
|
|
5200
5211
|
* the `run()` CnActionButtons bound for it, and announced to the host
|
|
5201
|
-
* with its declaration. A link action is left to the browser
|
|
5202
|
-
* a stage action this is the page's
|
|
5203
|
-
* before.
|
|
5212
|
+
* with its declaration. A link action is left to the browser and
|
|
5213
|
+
* announces nothing. Without a stage action this is the page's
|
|
5214
|
+
* `primaryAction`, announced as before.
|
|
5204
5215
|
*
|
|
5205
5216
|
* @return {void}
|
|
5206
5217
|
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
|
|
5218
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
5207
5219
|
*/
|
|
5208
5220
|
onPrimaryActionClick() {
|
|
5209
5221
|
const entry = this.stagePrimaryEntry
|
|
@@ -5211,6 +5223,9 @@ export default {
|
|
|
5211
5223
|
this.$emit('primary-action', this.primaryAction)
|
|
5212
5224
|
return
|
|
5213
5225
|
}
|
|
5226
|
+
if (entry.href) {
|
|
5227
|
+
return
|
|
5228
|
+
}
|
|
5214
5229
|
this.$emit('primary-action', this.headerActionsById[entry.id] || { id: entry.id, label: entry.label })
|
|
5215
5230
|
this.onPinnedActionClick(entry)
|
|
5216
5231
|
},
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
<NcActionLink
|
|
64
64
|
v-else-if="entry.href"
|
|
65
65
|
:href="entry.href"
|
|
66
|
-
target="_blank"
|
|
66
|
+
:target="entry.linkTarget || '_blank'"
|
|
67
67
|
rel="noopener noreferrer"
|
|
68
68
|
:data-testid="`cn-index-header-menu-${entry.key}`">
|
|
69
69
|
<template v-if="entry.icon" #icon>
|
|
@@ -103,6 +103,8 @@
|
|
|
103
103
|
<NcButton
|
|
104
104
|
v-else
|
|
105
105
|
:variant="button.variant"
|
|
106
|
+
:href="button.link ? button.link.href : undefined"
|
|
107
|
+
:target="button.link ? button.link.target : undefined"
|
|
106
108
|
:data-testid="`cn-index-header-button-${button.key}`"
|
|
107
109
|
@click="onHeaderButton(button)">
|
|
108
110
|
<template v-if="button.icon" #icon>
|
|
@@ -1347,8 +1349,8 @@ import { createRefLabelResolver } from '../../composables/useRefLabels.js'
|
|
|
1347
1349
|
import { useSavedViewsApi } from '../../composables/useSavedViewsApi.js'
|
|
1348
1350
|
import { METADATA_COLUMNS } from '../../constants/metadata.js'
|
|
1349
1351
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
1350
|
-
import { followItemActionLink, resolveItemActionLink, routeHref } from '../../utils/actionLink.js'
|
|
1351
|
-
import { buildOnSuccessRoute, resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
|
|
1352
|
+
import { followItemActionLink, headerActionAsLink, resolveItemActionLink, routeHref } from '../../utils/actionLink.js'
|
|
1353
|
+
import { buildOnSuccessRoute, isExternalActionTarget, resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
|
|
1352
1354
|
import {
|
|
1353
1355
|
buildReferenceStages,
|
|
1354
1356
|
buildValueStages,
|
|
@@ -3915,11 +3917,16 @@ export default {
|
|
|
3915
3917
|
* (unless the handler is the `'none'` keyword) emits
|
|
3916
3918
|
* `@header-action({ action: id, id })`.
|
|
3917
3919
|
*
|
|
3920
|
+
* An entry with `href` instead renders as a real link and is never
|
|
3921
|
+
* dispatched or emitted: an absolute `http(s)`, `mailto:`, `tel:` or
|
|
3922
|
+
* `//host/...` URL is used as written, an instance path (leading `/`) goes through
|
|
3923
|
+
* `generateUrl()`. `target: '_blank'` opens it in a new tab.
|
|
3924
|
+
*
|
|
3918
3925
|
* Reserved ids (those used by built-ins on the bar — `refresh`,
|
|
3919
3926
|
* `import`, `export`, `copy`, `delete`) are dropped from the
|
|
3920
3927
|
* merged list with a `console.warn`.
|
|
3921
3928
|
*
|
|
3922
|
-
* @type {Array<{ id: string, label: string, icon?: string, handler?: string|(() => void), route?: string, disabled?: boolean }>}
|
|
3929
|
+
* @type {Array<{ id: string, label: string, icon?: string, handler?: string|(() => void), route?: string, href?: string, target?: ('_blank'|'_self'), disabled?: boolean }>}
|
|
3923
3930
|
*/
|
|
3924
3931
|
headerActions: {
|
|
3925
3932
|
type: Array,
|
|
@@ -4372,7 +4379,7 @@ export default {
|
|
|
4372
4379
|
/**
|
|
4373
4380
|
* The header actions that open a dialog: `type: "open-form"` (the
|
|
4374
4381
|
* schema-driven create form, as the navigation's primary action opens
|
|
4375
|
-
* it) or `type: "open-modal"`, without a `handler` of their own.
|
|
4382
|
+
* it) or `type: "open-modal"`, without a `handler` or `href` of their own.
|
|
4376
4383
|
* Reserved ids are left out, as in `mergedHeaderActions`.
|
|
4377
4384
|
*
|
|
4378
4385
|
* @spec openspec/changes/index-header-open-form/specs/index-header/spec.md#requirement-an-index-header-action-can-open-the-create-form
|
|
@@ -4384,9 +4391,18 @@ export default {
|
|
|
4384
4391
|
&& typeof entry.id === 'string'
|
|
4385
4392
|
&& !reserved.has(entry.id)
|
|
4386
4393
|
&& (entry.handler === undefined || entry.handler === null || entry.handler === '')
|
|
4394
|
+
&& !entry.href
|
|
4387
4395
|
&& (entry.type === 'open-form' || entry.type === 'open-modal'))
|
|
4388
4396
|
},
|
|
4389
4397
|
|
|
4398
|
+
/**
|
|
4399
|
+
* The header actions without reserved ids, each through
|
|
4400
|
+
* `resolveHeaderHandler` (a manifest `href` entry becomes a link with
|
|
4401
|
+
* `_dispatchSuppress`, so the page neither runs nor emits it).
|
|
4402
|
+
*
|
|
4403
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
4404
|
+
* @return {Array<object>}
|
|
4405
|
+
*/
|
|
4390
4406
|
mergedHeaderActions() {
|
|
4391
4407
|
const reserved = new Set(['refresh', 'import', 'export', 'copy', 'delete'])
|
|
4392
4408
|
const merged = []
|
|
@@ -7425,11 +7441,13 @@ export default {
|
|
|
7425
7441
|
*
|
|
7426
7442
|
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
|
|
7427
7443
|
* @spec openspec/changes/screens-index-header-buttons-parity/specs/index-list-board-look/spec.md#requirement-the-add-header-button-carries-a-plus
|
|
7428
|
-
* @
|
|
7444
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
7445
|
+
* @return {Array<{key: string, label: string, action: string, variant: string, icon: string, format: string, link: ({href: string, target: (string|undefined)}|null)}>}
|
|
7429
7446
|
*/
|
|
7430
7447
|
resolvedHeaderButtons() {
|
|
7431
7448
|
const headerButtons = this.presented('headerButtons')
|
|
7432
7449
|
const declared = Array.isArray(headerButtons) ? headerButtons : []
|
|
7450
|
+
const builtInActions = new Set(['add', 'export', 'import', 'refresh', 'save-view', 'actions-menu'])
|
|
7433
7451
|
return declared
|
|
7434
7452
|
.filter((button) => button && typeof button === 'object' && typeof button.action === 'string' && button.action !== '')
|
|
7435
7453
|
.map((button, index) => {
|
|
@@ -7456,6 +7474,13 @@ export default {
|
|
|
7456
7474
|
if (label === '' && button.action === 'save-view') {
|
|
7457
7475
|
label = t('nextcloud-vue', 'Save view')
|
|
7458
7476
|
}
|
|
7477
|
+
const run = typeof button.run === 'string' ? button.run : ''
|
|
7478
|
+
// A button that runs a manifest `href` header action is that link;
|
|
7479
|
+
// a `type: "navigate"` or function-handler entry keeps dispatching.
|
|
7480
|
+
const actionId = button.action === 'export' ? run : (builtInActions.has(button.action) ? '' : button.action)
|
|
7481
|
+
const linked = actionId
|
|
7482
|
+
? this.mergedHeaderActions.find((entry) => entry && entry.id === actionId && entry.href && entry._dispatchSuppress && (!entry.type || entry.type === 'handler'))
|
|
7483
|
+
: null
|
|
7459
7484
|
return {
|
|
7460
7485
|
key: String(button.id || button.action || index),
|
|
7461
7486
|
label,
|
|
@@ -7463,7 +7488,8 @@ export default {
|
|
|
7463
7488
|
variant: button.variant === 'primary' ? 'primary' : 'secondary',
|
|
7464
7489
|
icon,
|
|
7465
7490
|
format: button.format === 'excel' ? 'excel' : 'csv',
|
|
7466
|
-
run
|
|
7491
|
+
run,
|
|
7492
|
+
link: linked ? { href: linked.href, target: linked.linkTarget || undefined } : null,
|
|
7467
7493
|
items: Array.isArray(button.items)
|
|
7468
7494
|
? button.items.filter((item) => (typeof item === 'string' && item !== '')
|
|
7469
7495
|
|| (item && typeof item === 'object' && typeof item.heading === 'string' && item.heading !== ''))
|
|
@@ -9121,8 +9147,11 @@ export default {
|
|
|
9121
9147
|
* manifest-actions-dispatch — `navigate`, `emit`, `none`, or a
|
|
9122
9148
|
* registry name (resolved through `resolveRegisteredHandler`).
|
|
9123
9149
|
*
|
|
9150
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
9124
9151
|
* @param {object} entry Raw headerActions entry.
|
|
9125
|
-
* @return {object} Possibly-mutated copy:
|
|
9152
|
+
* @return {object} Possibly-mutated copy: an `href` becomes a
|
|
9153
|
+
* resolved link with `linkTarget`, no handler (a function one
|
|
9154
|
+
* included) and `_dispatchSuppress: true`; function-typed
|
|
9126
9155
|
* handlers untouched; `'emit'` and unknown registry names
|
|
9127
9156
|
* strip the `handler` (emit-only fall-through); `'navigate'`
|
|
9128
9157
|
* becomes a `$router.push` thunk (with the entry's literal
|
|
@@ -9134,14 +9163,23 @@ export default {
|
|
|
9134
9163
|
if (!entry) {
|
|
9135
9164
|
return entry
|
|
9136
9165
|
}
|
|
9166
|
+
// An `href` makes the entry a link, which only navigates: it
|
|
9167
|
+
// carries no handler, and the page emits nothing for it.
|
|
9168
|
+
if (typeof entry.href === 'string' && entry.href !== '') {
|
|
9169
|
+
const link = headerActionAsLink(entry)
|
|
9170
|
+
return link.href ? { ...link, linkTarget: link.target, _dispatchSuppress: true } : link
|
|
9171
|
+
}
|
|
9137
9172
|
const handler = entry.handler
|
|
9138
9173
|
if (typeof handler === 'function') {
|
|
9139
9174
|
return { ...entry }
|
|
9140
9175
|
}
|
|
9141
9176
|
// `type: "navigate"` with a `target` URL is a link: the bar and the
|
|
9142
|
-
// header's Actions menu render it as a real anchor
|
|
9177
|
+
// header's Actions menu render it as a real anchor, an external URL
|
|
9178
|
+
// in a new tab.
|
|
9143
9179
|
if (typeof handler !== 'string' && entry.type === 'navigate' && typeof entry.target === 'string' && entry.target !== '' && !entry.href) {
|
|
9144
|
-
return
|
|
9180
|
+
return isExternalActionTarget(entry.target)
|
|
9181
|
+
? { ...entry, href: entry.target, linkTarget: '_blank' }
|
|
9182
|
+
: { ...entry, href: entry.target }
|
|
9145
9183
|
}
|
|
9146
9184
|
if (typeof handler !== 'string') {
|
|
9147
9185
|
// No handler declared — emit-only fall-through.
|
|
@@ -9312,8 +9350,9 @@ export default {
|
|
|
9312
9350
|
*
|
|
9313
9351
|
* @spec openspec/changes/screens-index-header-actions-menu/specs/index-header/spec.md#requirement-the-header-actions-menu-lists-what-its-items-name
|
|
9314
9352
|
* @spec openspec/changes/screens-index-header-menu-headings/specs/index-header/spec.md#requirement-the-header-actions-menu-can-draw-group-headings-and-separators
|
|
9353
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
9315
9354
|
* @param {{items?: Array<(string|{heading: string})>|null, format?: string}} button The resolved header button.
|
|
9316
|
-
* @return {Array<{key: string, label: string, kind?: ('heading'|'separator'), icon?: string, href?: string, disabled?: boolean, title?: string, iconComponent?: object, run?:
|
|
9355
|
+
* @return {Array<{key: string, label: string, kind?: ('heading'|'separator'), icon?: string, href?: string, linkTarget?: string, disabled?: boolean, title?: string, iconComponent?: object, run?: () => void}>}
|
|
9317
9356
|
*/
|
|
9318
9357
|
headerMenuEntries(button) {
|
|
9319
9358
|
const fromAction = (entry) => ({
|
|
@@ -9321,6 +9360,7 @@ export default {
|
|
|
9321
9360
|
label: entry.label ? this.cnTranslate(entry.label) : '',
|
|
9322
9361
|
icon: entry.icon && typeof entry.icon === 'string' && !entry.icon.startsWith('icon-') ? entry.icon : '',
|
|
9323
9362
|
href: typeof entry.href === 'string' && entry.href !== '' ? entry.href : '',
|
|
9363
|
+
linkTarget: entry.linkTarget || '',
|
|
9324
9364
|
// A `handler: "navigate"` entry whose route resolves carries `to`
|
|
9325
9365
|
// (resolveHeaderHandler drops its handler): the menu draws it as
|
|
9326
9366
|
// an in-app link, as the actions bar does, so it navigates.
|
|
@@ -9354,7 +9394,7 @@ export default {
|
|
|
9354
9394
|
}),
|
|
9355
9395
|
|
|
9356
9396
|
documentation: () => (this.documentationUrl
|
|
9357
|
-
? { key: 'documentation', label: this.documentationLabel || t('nextcloud-vue', 'Documentation'), icon: 'BookOpenVariantOutline', href: this.documentationUrl }
|
|
9397
|
+
? { key: 'documentation', label: this.documentationLabel || t('nextcloud-vue', 'Documentation'), icon: 'BookOpenVariantOutline', href: this.documentationUrl, linkTarget: '_blank' }
|
|
9358
9398
|
: null),
|
|
9359
9399
|
}
|
|
9360
9400
|
const entries = []
|
|
@@ -9450,13 +9490,18 @@ export default {
|
|
|
9450
9490
|
|
|
9451
9491
|
/**
|
|
9452
9492
|
* A header button was clicked: run its built-in action, or dispatch
|
|
9453
|
-
* it as the `headerActions` entry with that id.
|
|
9493
|
+
* it as the `headerActions` entry with that id. A button that is a
|
|
9494
|
+
* link is left to the browser.
|
|
9454
9495
|
*
|
|
9455
9496
|
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
|
|
9456
|
-
* @
|
|
9497
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
9498
|
+
* @param {{action: string, format: string, link?: object}} button The resolved button.
|
|
9457
9499
|
* @return {void}
|
|
9458
9500
|
*/
|
|
9459
9501
|
onHeaderButton(button) {
|
|
9502
|
+
if (button.link) {
|
|
9503
|
+
return
|
|
9504
|
+
}
|
|
9460
9505
|
if (button.action === 'add') {
|
|
9461
9506
|
this.onAddClick()
|
|
9462
9507
|
} else if (button.action === 'export') {
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
<NcButton
|
|
49
49
|
v-else
|
|
50
50
|
:variant="button.variant"
|
|
51
|
+
:href="button.link ? button.link.href : undefined"
|
|
52
|
+
:target="button.link ? button.link.target : undefined"
|
|
51
53
|
:data-testid="`cn-reports-header-button-${button.key}`"
|
|
52
54
|
@click="onHeaderButton(button)">
|
|
53
55
|
<template v-if="button.icon" #icon>
|
|
@@ -163,6 +165,7 @@ import { defineAsyncComponent } from 'vue'
|
|
|
163
165
|
import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
|
|
164
166
|
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
165
167
|
import { useLook } from '../../composables/useLook.js'
|
|
168
|
+
import { headerActionAsLink } from '../../utils/actionLink.js'
|
|
166
169
|
import { resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
|
|
167
170
|
import { followLinkClick } from '../../utils/linkNavigation.js'
|
|
168
171
|
|
|
@@ -325,7 +328,11 @@ export default {
|
|
|
325
328
|
* function, `navigate` (with `route`, and `params`), `emit`, `none`,
|
|
326
329
|
* or a registry name; `type: "navigate"` with a `target` URL opens
|
|
327
330
|
* that URL in a new tab. Every click also emits `header-action`
|
|
328
|
-
* `{ action, id }`, except for `handler: "none"`.
|
|
331
|
+
* `{ action, id }`, except for `handler: "none"`. An entry with `href`
|
|
332
|
+
* makes its button a real link that runs and emits nothing: an
|
|
333
|
+
* absolute `http(s)`, `mailto:`, `tel:` or `//host/...` URL as written,
|
|
334
|
+
* an instance path (leading `/`) through `generateUrl()`, in a new tab with
|
|
335
|
+
* `target: '_blank'`.
|
|
329
336
|
*
|
|
330
337
|
* @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
|
|
331
338
|
* @type {Array<object>|null}
|
|
@@ -451,24 +458,31 @@ export default {
|
|
|
451
458
|
|
|
452
459
|
/**
|
|
453
460
|
* The declared header buttons that have a label and an action,
|
|
454
|
-
* translated, each with a stable key and a variant.
|
|
461
|
+
* translated, each with a stable key and a variant. A button naming a
|
|
462
|
+
* header action with `href` carries that `link`.
|
|
455
463
|
*
|
|
456
464
|
* @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
|
|
457
|
-
* @
|
|
465
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
466
|
+
* @return {Array<{key: string, label: string, action: string, variant: string, icon: string, link: ({href: string, target: string}|null)}>} The buttons.
|
|
458
467
|
*/
|
|
459
468
|
resolvedHeaderButtons() {
|
|
460
469
|
const declared = Array.isArray(this.config.headerButtons) ? this.config.headerButtons : []
|
|
470
|
+
const actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []
|
|
461
471
|
return declared
|
|
462
472
|
.filter((button) => button && typeof button === 'object'
|
|
463
473
|
&& typeof button.action === 'string' && button.action !== ''
|
|
464
474
|
&& typeof button.label === 'string' && button.label !== '')
|
|
465
|
-
.map((button) =>
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
475
|
+
.map((button) => {
|
|
476
|
+
const entry = headerActionAsLink(actions.find((action) => action && action.id === button.action))
|
|
477
|
+
return {
|
|
478
|
+
key: String(button.id || button.action),
|
|
479
|
+
label: this.tr(button.label),
|
|
480
|
+
action: button.action,
|
|
481
|
+
variant: button.variant === 'primary' ? 'primary' : 'secondary',
|
|
482
|
+
icon: typeof button.icon === 'string' ? button.icon : '',
|
|
483
|
+
link: entry && typeof entry.href === 'string' && entry.href !== '' ? { href: entry.href, target: entry.target } : null,
|
|
484
|
+
}
|
|
485
|
+
})
|
|
472
486
|
},
|
|
473
487
|
|
|
474
488
|
/**
|
|
@@ -685,13 +699,18 @@ export default {
|
|
|
685
699
|
|
|
686
700
|
/**
|
|
687
701
|
* A header button was clicked: run the `headerActions` entry it names
|
|
688
|
-
* and emit `header-action`, as an index page's header button does.
|
|
702
|
+
* and emit `header-action`, as an index page's header button does. A
|
|
703
|
+
* button that is a link is left to the browser.
|
|
689
704
|
*
|
|
690
705
|
* @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
|
|
691
|
-
* @
|
|
706
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
707
|
+
* @param {{action: string, link?: object}} button The resolved button.
|
|
692
708
|
* @return {void}
|
|
693
709
|
*/
|
|
694
710
|
onHeaderButton(button) {
|
|
711
|
+
if (button.link) {
|
|
712
|
+
return
|
|
713
|
+
}
|
|
695
714
|
const id = button.action
|
|
696
715
|
const actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []
|
|
697
716
|
const entry = actions.find((action) => action && action.id === id) || null
|
package/src/css/badge.css
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
======================================== */
|
|
4
4
|
|
|
5
5
|
/* The two board tones without a Nextcloud colour role (screens-cell-pill-parity).
|
|
6
|
-
A theme may redefine them
|
|
6
|
+
A theme may redefine them, or name the NL Design System hue tint and pill
|
|
7
|
+
text (`--nl-color-<hue>-light`, `--nl-color-<hue>-text`), which the tones
|
|
8
|
+
and the four role variants below read first (status-badge-hue-tokens). */
|
|
7
9
|
:root {
|
|
8
|
-
--cn-status-purple-bg: #e8e6f6;
|
|
9
|
-
--cn-status-purple-text: #4a3f8f;
|
|
10
|
-
--cn-status-teal-bg: #e3f1f3;
|
|
11
|
-
--cn-status-teal-text: #1d5e66;
|
|
10
|
+
--cn-status-purple-bg: var(--nl-color-purple-light, #e8e6f6);
|
|
11
|
+
--cn-status-purple-text: var(--nl-color-purple-text, #4a3f8f);
|
|
12
|
+
--cn-status-teal-bg: var(--nl-color-teal-light, #e3f1f3);
|
|
13
|
+
--cn-status-teal-text: var(--nl-color-teal-text, #1d5e66);
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
.cn-status-badge {
|
|
@@ -43,26 +45,26 @@
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
.cn-status-badge--success {
|
|
46
|
-
--cn-status-badge-ink: var(--color-success-text, var(--color-success));
|
|
47
|
-
background-color: var(--color-success-light, rgba(70, 186, 97, 0.15));
|
|
48
|
+
--cn-status-badge-ink: var(--nl-color-green-text, var(--color-success-text, var(--color-success)));
|
|
49
|
+
background-color: var(--nl-color-green-light, var(--color-success-light, rgba(70, 186, 97, 0.15)));
|
|
48
50
|
color: var(--cn-status-badge-ink);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
.cn-status-badge--warning {
|
|
52
|
-
--cn-status-badge-ink: var(--color-warning-text, var(--color-warning));
|
|
53
|
-
background-color: var(--color-warning-light, rgba(232, 163, 39, 0.15));
|
|
54
|
+
--cn-status-badge-ink: var(--nl-color-orange-text, var(--color-warning-text, var(--color-warning)));
|
|
55
|
+
background-color: var(--nl-color-orange-light, var(--color-warning-light, rgba(232, 163, 39, 0.15)));
|
|
54
56
|
color: var(--cn-status-badge-ink);
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
.cn-status-badge--error {
|
|
58
|
-
--cn-status-badge-ink: var(--color-error-text, var(--color-error));
|
|
59
|
-
background-color: var(--color-error-light, rgba(224, 68, 68, 0.15));
|
|
60
|
+
--cn-status-badge-ink: var(--nl-color-red-text, var(--color-error-text, var(--color-error)));
|
|
61
|
+
background-color: var(--nl-color-red-light, var(--color-error-light, rgba(224, 68, 68, 0.15)));
|
|
60
62
|
color: var(--cn-status-badge-ink);
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
.cn-status-badge--info {
|
|
64
|
-
--cn-status-badge-ink: var(--color-info-text, var(--color-info, #0082c9));
|
|
65
|
-
background-color: var(--color-info-light, rgba(0, 130, 201, 0.15));
|
|
66
|
+
--cn-status-badge-ink: var(--nl-color-blue-text, var(--color-info-text, var(--color-info, #0082c9)));
|
|
67
|
+
background-color: var(--nl-color-blue-light, var(--color-info-light, rgba(0, 130, 201, 0.15)));
|
|
66
68
|
color: var(--cn-status-badge-ink);
|
|
67
69
|
}
|
|
68
70
|
|
package/src/css/board.css
CHANGED
|
@@ -55,7 +55,6 @@
|
|
|
55
55
|
margin-inline-start: auto;
|
|
56
56
|
padding: 1px 8px;
|
|
57
57
|
border-radius: 10px;
|
|
58
|
-
--nldesign-color-on-surface: var(--cn-board-text-soft, var(--color-main-text));
|
|
59
58
|
background-color: var(--color-main-background);
|
|
60
59
|
color: var(--cn-board-text-soft, var(--color-main-text));
|
|
61
60
|
font-size: 12px;
|
|
@@ -205,26 +205,19 @@
|
|
|
205
205
|
outline-offset: 2px;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
/* The selected chip is light on dark
|
|
209
|
-
--nldesign-color-on-surface: thematiq forces the body ink onto every span
|
|
210
|
-
with `!important` and reads it from that property, which left the label
|
|
211
|
-
dark on dark. */
|
|
208
|
+
/* The selected chip is light on dark. */
|
|
212
209
|
.cn-look-board .cn-saved-views__chips .cn-saved-views__chip--selected,
|
|
213
210
|
.cn-look-board .cn-saved-views__chips .cn-saved-views__chip--selected:hover,
|
|
214
211
|
.cn-look-board .cn-quick-filter-bar .cn-quick-filter-bar__tab--active,
|
|
215
212
|
.cn-look-board .cn-quick-filter-bar .cn-quick-filter-bar__tab--active:hover {
|
|
216
|
-
--nldesign-color-on-surface: var(--color-main-background);
|
|
217
213
|
border-color: transparent;
|
|
218
214
|
background: var(--color-main-text);
|
|
219
215
|
color: var(--color-main-background);
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
/* The count badge: grey on an idle chip, soft text colour on the selected one.
|
|
223
|
-
Its ink also goes to --nldesign-color-on-surface, which a theme that forces
|
|
224
|
-
the body ink onto every span (thematiq) reads. */
|
|
218
|
+
/* The count badge: grey on an idle chip, soft text colour on the selected one. */
|
|
225
219
|
.cn-look-board .cn-saved-views__chip .cn-saved-views__chip-count,
|
|
226
220
|
.cn-look-board .cn-quick-filter-bar .cn-quick-filter-bar__count {
|
|
227
|
-
--nldesign-color-on-surface: var(--cn-board-text-soft);
|
|
228
221
|
min-width: 0;
|
|
229
222
|
padding: 1px 7px;
|
|
230
223
|
border: 0;
|
|
@@ -242,7 +235,6 @@
|
|
|
242
235
|
/* On the selected chip the count is the soft ink as a lighter bubble on the
|
|
243
236
|
dark chip (DqZaken: #3d4047 on #1b1c1d). */
|
|
244
237
|
.cn-look-board .cn-quick-filter-bar .cn-quick-filter-bar__tab--active .cn-quick-filter-bar__count {
|
|
245
|
-
--nldesign-color-on-surface: var(--color-primary-element-text);
|
|
246
238
|
background: var(--cn-board-text-soft);
|
|
247
239
|
color: var(--color-primary-element-text);
|
|
248
240
|
}
|
|
@@ -322,8 +314,6 @@
|
|
|
322
314
|
margin-inline-start: 6px;
|
|
323
315
|
padding: 0 6px;
|
|
324
316
|
border-radius: 10px;
|
|
325
|
-
/* A theme that forces the body ink onto every span reads it from here. */
|
|
326
|
-
--nldesign-color-on-surface: var(--color-primary-element-text);
|
|
327
317
|
background: var(--color-primary-element);
|
|
328
318
|
color: var(--color-primary-element-text);
|
|
329
319
|
font-size: 12px;
|
|
@@ -553,15 +543,13 @@
|
|
|
553
543
|
}
|
|
554
544
|
|
|
555
545
|
/* The header row is near-white on the boards (#fafbfc), not the hover grey,
|
|
556
|
-
its labels 14px muted.
|
|
557
|
-
which a theme that forces the body ink onto every span (thematiq) reads.
|
|
546
|
+
its labels 14px muted.
|
|
558
547
|
The nldesign theme paints every `th` with `!important` through its table
|
|
559
548
|
header tokens, and the sort button in it through its button weight token
|
|
560
549
|
(bold), so the cell hands the board's ground, ink and weight to those
|
|
561
550
|
tokens too.
|
|
562
551
|
@spec openspec/changes/board-table-head-theme/specs/board-table-head-theme/spec.md#requirement-a-board-table-header-keeps-the-boards-ground-ink-and-weight-under-a-theme */
|
|
563
552
|
.cn-look-board .cn-table-container--board .cn-data-table th {
|
|
564
|
-
--nldesign-color-on-surface: var(--color-text-maxcontrast);
|
|
565
553
|
--nldesign-component-table-header-background-color: var(--cn-board-table-head-bg, color-mix(in srgb, var(--color-background-hover) 40%, var(--color-main-background)));
|
|
566
554
|
--nldesign-component-table-header-color: var(--color-text-maxcontrast);
|
|
567
555
|
--nldesign-component-table-header-font-weight: 600;
|
|
@@ -695,7 +683,6 @@
|
|
|
695
683
|
}
|
|
696
684
|
|
|
697
685
|
.cn-look-board .cn-table-container--board .cn-data-table td.cn-table-col--title .cn-table-cell__secondary {
|
|
698
|
-
--nldesign-color-on-surface: var(--color-text-maxcontrast);
|
|
699
686
|
color: var(--color-text-maxcontrast);
|
|
700
687
|
font-size: 14px;
|
|
701
688
|
font-weight: 400;
|
|
@@ -740,7 +727,6 @@
|
|
|
740
727
|
/* The initials sit on the light primary tint in the primary ink, the board's
|
|
741
728
|
person chip (DqZaken's Behandelaar, and most boards' people). */
|
|
742
729
|
.cn-look-board .cn-table-container--board .cn-data-table .cn-cell-renderer__avatar-initials {
|
|
743
|
-
--nldesign-color-on-surface: var(--color-primary-element-light-text);
|
|
744
730
|
background: var(--color-primary-element-light);
|
|
745
731
|
color: var(--color-primary-element-light-text);
|
|
746
732
|
font-size: 12px;
|