@conduction/nextcloud-vue 2.74.0 → 2.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +19 -14
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +2 -2
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +77 -26
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +22 -14
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +23 -4
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue.js +9 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue.js.map +1 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js +107 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js +79 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -0
- package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js +4 -1
- package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js.map +1 -1
- package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +14 -7
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +32 -0
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +6 -3
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +18 -2
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +2 -2
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +16 -0
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js +2 -2
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
- package/dist/esm/directives/cnSelectAria.js +55 -0
- package/dist/esm/directives/cnSelectAria.js.map +1 -0
- package/dist/esm/nextcloud-vue.css +66 -1
- package/dist/esm/utils/cnFetch.js +17 -2
- package/dist/esm/utils/cnFetch.js.map +1 -1
- package/dist/esm/utils/resolveBrand.js +38 -0
- package/dist/esm/utils/resolveBrand.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +8775 -8327
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +66 -1
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +21 -16
- package/src/components/CnAppRoot/CnAppRoot.vue +53 -0
- package/src/components/CnBannerWidget/CnBannerWidget.vue +23 -4
- package/src/components/CnBrandBar/CnBrandBar.vue +230 -0
- package/src/components/CnEnvironmentBanner/CnEnvironmentBanner.vue +4 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +10 -0
- package/src/components/CnFormPage/CnFormPage.vue +33 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +2 -2
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +20 -4
- package/src/components/CnStatsBlock/CnStatsBlock.vue +18 -2
- package/src/css/form-field.css +1 -1
- package/src/directives/cnSelectAria.js +54 -0
- package/src/schemas/app-manifest-v2.schema.json +6 -1
- package/src/utils/cnFetch.js +16 -1
- package/src/utils/resolveBrand.js +37 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/dist/nextcloud-vue.cjs.js.map
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 38285312 bytes
|
|
4
|
+
sha256: 91421df55f0b2ab43c1c2b9428d1808f048aa4d3a8a0909a9681e4f49b38b627
|
package/dist/nextcloud-vue.css
CHANGED
|
@@ -795,7 +795,7 @@
|
|
|
795
795
|
}
|
|
796
796
|
|
|
797
797
|
/* An invalid field: 2px error border on the control, 4px edge on the group. */
|
|
798
|
-
.cn-look-board .cn-form-field--invalid input,
|
|
798
|
+
.cn-look-board .cn-form-field--invalid input:not([type='checkbox']):not([type='radio']),
|
|
799
799
|
.cn-look-board .cn-form-field--invalid textarea,
|
|
800
800
|
.cn-look-board .cn-form-field--invalid .select {
|
|
801
801
|
border: var(--cn-field-error-border) solid var(--color-error);
|
|
@@ -38840,6 +38840,71 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
|
|
|
38840
38840
|
}
|
|
38841
38841
|
|
|
38842
38842
|
|
|
38843
|
+
/* The host is one flex item at the start of Nextcloud's header. */
|
|
38844
|
+
.cn-brand-bar-host {
|
|
38845
|
+
flex: none;
|
|
38846
|
+
display: flex;
|
|
38847
|
+
align-items: center;
|
|
38848
|
+
height: 100%;
|
|
38849
|
+
margin-inline-end: 12px;
|
|
38850
|
+
}
|
|
38851
|
+
.cn-brand-bar {
|
|
38852
|
+
box-sizing: border-box;
|
|
38853
|
+
display: flex;
|
|
38854
|
+
align-items: center;
|
|
38855
|
+
gap: 12px;
|
|
38856
|
+
width: var(--cn-board-brand-width, 237px);
|
|
38857
|
+
}
|
|
38858
|
+
.cn-brand-bar__brand {
|
|
38859
|
+
flex: 1 1 0;
|
|
38860
|
+
box-sizing: border-box;
|
|
38861
|
+
display: flex;
|
|
38862
|
+
align-items: center;
|
|
38863
|
+
gap: 10px;
|
|
38864
|
+
min-width: 0;
|
|
38865
|
+
min-height: 48px;
|
|
38866
|
+
max-height: 100%;
|
|
38867
|
+
padding: 0 6px;
|
|
38868
|
+
border-radius: 8px;
|
|
38869
|
+
color: var(--color-background-plain-text, var(--color-main-text));
|
|
38870
|
+
}
|
|
38871
|
+
.cn-brand-bar__emblem {
|
|
38872
|
+
flex: none;
|
|
38873
|
+
height: var(--cn-nav-emblem-size, 34px);
|
|
38874
|
+
width: auto;
|
|
38875
|
+
object-fit: contain;
|
|
38876
|
+
}
|
|
38877
|
+
.cn-brand-bar__emblem--theme {
|
|
38878
|
+
display: inline-block;
|
|
38879
|
+
width: var(--cn-nav-emblem-size, 34px);
|
|
38880
|
+
background: var(--nldesign-emblem-url) center / contain no-repeat;
|
|
38881
|
+
}
|
|
38882
|
+
.cn-brand-bar__text {
|
|
38883
|
+
display: flex;
|
|
38884
|
+
flex-direction: column;
|
|
38885
|
+
line-height: 1.15;
|
|
38886
|
+
min-width: 0;
|
|
38887
|
+
}
|
|
38888
|
+
.cn-brand-bar__caption {
|
|
38889
|
+
font-size: 13px;
|
|
38890
|
+
white-space: nowrap;
|
|
38891
|
+
opacity: 0.8;
|
|
38892
|
+
}
|
|
38893
|
+
.cn-brand-bar__name {
|
|
38894
|
+
font-size: 18px;
|
|
38895
|
+
font-weight: 700;
|
|
38896
|
+
white-space: nowrap;
|
|
38897
|
+
}
|
|
38898
|
+
.cn-brand-bar__divider {
|
|
38899
|
+
flex: none;
|
|
38900
|
+
width: 1px;
|
|
38901
|
+
height: 28px;
|
|
38902
|
+
background: currentColor;
|
|
38903
|
+
opacity: 0.25;
|
|
38904
|
+
color: var(--color-background-plain-text, var(--color-main-text));
|
|
38905
|
+
}
|
|
38906
|
+
|
|
38907
|
+
|
|
38843
38908
|
.cn-notif-prefs__intro[data-v-72cc0056] {
|
|
38844
38909
|
margin-bottom: 12px;
|
|
38845
38910
|
color: var(--color-text-maxcontrast);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.75.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": {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
-->
|
|
76
76
|
<template>
|
|
77
77
|
<NcAppNavigation :aria-label="ariaLabel" data-testid="cn-nav">
|
|
78
|
-
<template v-if="$slots.search || $slots.brand ||
|
|
78
|
+
<template v-if="$slots.search || $slots.brand || navBrand" #search>
|
|
79
79
|
<!--
|
|
80
80
|
@slot brand
|
|
81
81
|
@description Replace the brand block at the very top of the
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
-->
|
|
87
87
|
<slot name="brand" :brand="resolvedBrand">
|
|
88
88
|
<div
|
|
89
|
-
v-if="
|
|
89
|
+
v-if="navBrand"
|
|
90
90
|
class="cn-app-nav__brand"
|
|
91
91
|
data-testid="cn-nav-brand">
|
|
92
92
|
<!-- Decorative beside a name: the name already says whose
|
|
@@ -481,6 +481,7 @@ import CnMenuItemIcon from '../CnMenuWidget/CnMenuItemIcon.vue'
|
|
|
481
481
|
import { useLook } from '../../composables/useLook.js'
|
|
482
482
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
483
483
|
import { isSvgPath } from '../../utils/iconUtils.js'
|
|
484
|
+
import { resolveBrand } from '../../utils/resolveBrand.js'
|
|
484
485
|
import { passesContextPredicates } from '../../utils/visibleIfContext.js'
|
|
485
486
|
import { CnActionButtons } from '../CnActionButtons/index.js'
|
|
486
487
|
// The legacy `icon-*` → MDI bridge lives beside CnIcon now, so the menu EDITOR
|
|
@@ -579,6 +580,12 @@ export default {
|
|
|
579
580
|
|
|
580
581
|
inject: {
|
|
581
582
|
cnManifest: { default: null },
|
|
583
|
+
/**
|
|
584
|
+
* Provided by CnAppRoot as true while the board look draws the brand
|
|
585
|
+
* block in Nextcloud's header (screens-brand-block-top-bar): the
|
|
586
|
+
* navigation then skips its own.
|
|
587
|
+
*/
|
|
588
|
+
cnBrandInHeader: { default: false },
|
|
582
589
|
cnTranslate: { default: () => (key) => key },
|
|
583
590
|
/**
|
|
584
591
|
* Provided by CnAppRoot — opens the host app's
|
|
@@ -808,6 +815,17 @@ export default {
|
|
|
808
815
|
return this.translate ?? this.cnTranslate
|
|
809
816
|
},
|
|
810
817
|
|
|
818
|
+
/**
|
|
819
|
+
* The brand the navigation draws itself: the resolved brand, or null
|
|
820
|
+
* while the app's top bar draws it.
|
|
821
|
+
*
|
|
822
|
+
* @spec openspec/changes/screens-brand-block-top-bar/specs/layout-components/spec.md#requirement-the-navigation-does-not-draw-a-brand-block-the-bar-already-draws
|
|
823
|
+
* @return {object|null}
|
|
824
|
+
*/
|
|
825
|
+
navBrand() {
|
|
826
|
+
return this.cnBrandInHeader ? null : this.resolvedBrand
|
|
827
|
+
},
|
|
828
|
+
|
|
811
829
|
/**
|
|
812
830
|
* The brand to draw: the `brand` prop, else the manifest's
|
|
813
831
|
* `nav.brand`. Name and caption go through the translate function,
|
|
@@ -819,20 +837,7 @@ export default {
|
|
|
819
837
|
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-navigation-brand
|
|
820
838
|
*/
|
|
821
839
|
resolvedBrand() {
|
|
822
|
-
|
|
823
|
-
if (!declared || typeof declared !== 'object') {
|
|
824
|
-
return null
|
|
825
|
-
}
|
|
826
|
-
const text = (value) => (typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : '')
|
|
827
|
-
const brand = {
|
|
828
|
-
logo: typeof declared.logo === 'string' ? declared.logo : '',
|
|
829
|
-
// A URL, or `true` for the theme's emblem (--nldesign-emblem-url).
|
|
830
|
-
emblem: declared.emblem === true ? true : (typeof declared.emblem === 'string' ? declared.emblem : ''),
|
|
831
|
-
name: text(declared.name),
|
|
832
|
-
caption: text(declared.caption),
|
|
833
|
-
alt: text(declared.alt),
|
|
834
|
-
}
|
|
835
|
-
return (brand.logo || brand.emblem || brand.name || brand.caption) ? brand : null
|
|
840
|
+
return resolveBrand(this.brand ?? this.effectiveManifest?.nav?.brand, (value) => this.effectiveTranslate(value))
|
|
836
841
|
},
|
|
837
842
|
|
|
838
843
|
/**
|
|
@@ -189,6 +189,8 @@
|
|
|
189
189
|
|
|
190
190
|
<!-- Phase 3: shell -->
|
|
191
191
|
<template v-else>
|
|
192
|
+
<!-- The brand block in Nextcloud's own header (board look); teleported, renders nothing here. -->
|
|
193
|
+
<CnBrandBar v-if="brandInHeader" :brand="resolvedBrandBlock" @unavailable="headerMissing = true" />
|
|
192
194
|
<!--
|
|
193
195
|
@slot menu
|
|
194
196
|
@description Left-rail navigation surface. Default:
|
|
@@ -681,6 +683,7 @@ import Restart from 'vue-material-design-icons/Restart.vue'
|
|
|
681
683
|
import CnAiCompanion from '../CnAiCompanion/CnAiCompanion.vue'
|
|
682
684
|
import CnAppLoading from '../CnAppLoading/CnAppLoading.vue'
|
|
683
685
|
import CnAppNav from '../CnAppNav/CnAppNav.vue'
|
|
686
|
+
import CnBrandBar from '../CnBrandBar/CnBrandBar.vue'
|
|
684
687
|
import CnCommandPalette from '../CnCommandPalette/CnCommandPalette.vue'
|
|
685
688
|
import CnCredentials from '../CnCredentials/CnCredentials.vue'
|
|
686
689
|
import CnDependencyMissing from '../CnDependencyMissing/CnDependencyMissing.vue'
|
|
@@ -721,6 +724,7 @@ import { DEFAULT_FORGE, resolveForge } from '../../utils/forge.js'
|
|
|
721
724
|
import { BUILT_IN_KB_PROVIDERS } from '../../utils/kbSearchProviders.js'
|
|
722
725
|
import { createManifestTranslate } from '../../utils/manifestTranslate.js'
|
|
723
726
|
import { installModalStack, uninstallModalStack } from '../../utils/modalStack.js'
|
|
727
|
+
import { resolveBrand } from '../../utils/resolveBrand.js'
|
|
724
728
|
import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
725
729
|
import { passesContextPredicates } from '../../utils/visibleIfContext.js'
|
|
726
730
|
|
|
@@ -781,6 +785,7 @@ export default {
|
|
|
781
785
|
name: 'CnAppRoot',
|
|
782
786
|
|
|
783
787
|
components: {
|
|
788
|
+
CnBrandBar,
|
|
784
789
|
NcAppContent,
|
|
785
790
|
NcAppSettingsDialog,
|
|
786
791
|
NcAppSettingsSection,
|
|
@@ -831,6 +836,9 @@ export default {
|
|
|
831
836
|
// value once, so a getter would freeze the look at creation.
|
|
832
837
|
cnLook: computed(() => self.resolvedLook),
|
|
833
838
|
|
|
839
|
+
// True while CnBrandBar draws the block in Nextcloud's header, so CnAppNav skips its own.
|
|
840
|
+
cnBrandInHeader: computed(() => self.brandInHeader),
|
|
841
|
+
|
|
834
842
|
get cnManifest() {
|
|
835
843
|
return self.manifestEditor ? self.manifestEditor.source.value : self.manifest
|
|
836
844
|
},
|
|
@@ -1591,6 +1599,19 @@ export default {
|
|
|
1591
1599
|
default: '',
|
|
1592
1600
|
},
|
|
1593
1601
|
|
|
1602
|
+
/**
|
|
1603
|
+
* The brand block (`{ logo, emblem, name, caption, alt, placement }`)
|
|
1604
|
+
* the board look draws at the start of Nextcloud's header. Empty falls back
|
|
1605
|
+
* to the manifest's `nav.brand`. `placement: "nav"` keeps the block
|
|
1606
|
+
* in the navigation.
|
|
1607
|
+
*
|
|
1608
|
+
* @type {object|null}
|
|
1609
|
+
*/
|
|
1610
|
+
brand: {
|
|
1611
|
+
type: Object,
|
|
1612
|
+
default: null,
|
|
1613
|
+
},
|
|
1614
|
+
|
|
1594
1615
|
/**
|
|
1595
1616
|
* Title rendered at the top of the user-settings modal
|
|
1596
1617
|
* (NcAppSettingsDialog `name` prop). Defaults to the
|
|
@@ -1790,6 +1811,12 @@ export default {
|
|
|
1790
1811
|
data() {
|
|
1791
1812
|
const willCheck = Array.isArray(this.requiresApps) && this.requiresApps.length > 0
|
|
1792
1813
|
return {
|
|
1814
|
+
// Nextcloud's own header, present on a server-rendered page; the
|
|
1815
|
+
// brand block goes into it (CnBrandBar). Read once at creation so
|
|
1816
|
+
// the navigation never draws a block that then moves.
|
|
1817
|
+
headerPresent: typeof document !== 'undefined' && !!document.getElementById('header'),
|
|
1818
|
+
// Set when CnBrandBar finds the header gone: the navigation draws the block.
|
|
1819
|
+
headerMissing: false,
|
|
1793
1820
|
capabilitiesLoading: willCheck,
|
|
1794
1821
|
missingApps: [],
|
|
1795
1822
|
guardError: null,
|
|
@@ -2062,6 +2089,32 @@ export default {
|
|
|
2062
2089
|
return (this.look || this.manifest?.look) === 'board' ? 'board' : 'nextcloud'
|
|
2063
2090
|
},
|
|
2064
2091
|
|
|
2092
|
+
/**
|
|
2093
|
+
* The brand CnBrandBar draws in the header: the `brand` prop, else the manifest's
|
|
2094
|
+
* `nav.brand`, read into one shape.
|
|
2095
|
+
*
|
|
2096
|
+
* @return {object|null} The resolved brand, or null.
|
|
2097
|
+
*/
|
|
2098
|
+
resolvedBrandBlock() {
|
|
2099
|
+
return resolveBrand(this.brand || this.manifest?.nav?.brand, (text) => this.manifestTranslate(text))
|
|
2100
|
+
},
|
|
2101
|
+
|
|
2102
|
+
/**
|
|
2103
|
+
* Whether CnBrandBar draws the brand block in Nextcloud's header: the
|
|
2104
|
+
* board look, a brand that can be drawn, a placement other than `nav`,
|
|
2105
|
+
* and a `#header` to put it in. Without `#header` the navigation draws it.
|
|
2106
|
+
*
|
|
2107
|
+
* @spec openspec/changes/screens-brand-block-top-bar/specs/layout-components/spec.md#requirement-the-board-look-draws-the-brand-block-in-the-nextcloud-header
|
|
2108
|
+
* @return {boolean}
|
|
2109
|
+
*/
|
|
2110
|
+
brandInHeader() {
|
|
2111
|
+
return this.resolvedLook === 'board'
|
|
2112
|
+
&& !this.headerMissing
|
|
2113
|
+
&& this.headerPresent
|
|
2114
|
+
&& !!this.resolvedBrandBlock
|
|
2115
|
+
&& this.resolvedBrandBlock.placement !== 'nav'
|
|
2116
|
+
},
|
|
2117
|
+
|
|
2065
2118
|
/**
|
|
2066
2119
|
* The label lookup provided as `cnTranslate`: the manifest's own
|
|
2067
2120
|
* translations for the language first, then the host `translate`, then the
|
|
@@ -142,6 +142,14 @@ export default {
|
|
|
142
142
|
|
|
143
143
|
components: { NcNoteCard },
|
|
144
144
|
|
|
145
|
+
inject: {
|
|
146
|
+
/**
|
|
147
|
+
* The host's label lookup (CnAppRoot provides it); identity without one, so a
|
|
148
|
+
* label that is not a key renders as written.
|
|
149
|
+
*/
|
|
150
|
+
cnTranslate: { default: () => (key) => key },
|
|
151
|
+
},
|
|
152
|
+
|
|
145
153
|
props: {
|
|
146
154
|
/**
|
|
147
155
|
* Banner severity variant: `info | warning | error` (plus `success`
|
|
@@ -362,7 +370,7 @@ export default {
|
|
|
362
370
|
*/
|
|
363
371
|
resolvedKicker() {
|
|
364
372
|
const kicker = this.kicker || (this.content && this.content.kicker) || ''
|
|
365
|
-
return kicker
|
|
373
|
+
return kicker ? this.tl(kicker) : ''
|
|
366
374
|
},
|
|
367
375
|
|
|
368
376
|
/**
|
|
@@ -373,7 +381,7 @@ export default {
|
|
|
373
381
|
*/
|
|
374
382
|
displayTitle() {
|
|
375
383
|
const title = this.title || (this.content && this.content.title) || ''
|
|
376
|
-
return title ? this.fillValue(title) : this.displayText
|
|
384
|
+
return title ? this.fillValue(this.tl(title)) : this.displayText
|
|
377
385
|
},
|
|
378
386
|
|
|
379
387
|
/**
|
|
@@ -386,7 +394,7 @@ export default {
|
|
|
386
394
|
displayReason() {
|
|
387
395
|
const reason = this.reason || (this.content && this.content.reason) || ''
|
|
388
396
|
if (reason) {
|
|
389
|
-
return this.fillValue(reason)
|
|
397
|
+
return this.fillValue(this.tl(reason))
|
|
390
398
|
}
|
|
391
399
|
const title = this.title || (this.content && this.content.title) || ''
|
|
392
400
|
// A card that sets `text` to the same words as its `title` says
|
|
@@ -424,7 +432,7 @@ export default {
|
|
|
424
432
|
return {
|
|
425
433
|
key: `${index}-${action.id || action.label}`,
|
|
426
434
|
id: action.id,
|
|
427
|
-
label: action.label,
|
|
435
|
+
label: this.tl(action.label),
|
|
428
436
|
href,
|
|
429
437
|
target,
|
|
430
438
|
primary: index === primaryIndex,
|
|
@@ -526,6 +534,17 @@ export default {
|
|
|
526
534
|
},
|
|
527
535
|
|
|
528
536
|
methods: {
|
|
537
|
+
/**
|
|
538
|
+
* A manifest string through the host's label lookup (`cnTranslate`), so a key
|
|
539
|
+
* such as "Mine" reads in the user's language.
|
|
540
|
+
*
|
|
541
|
+
* @param {string} text The text as written in the manifest.
|
|
542
|
+
* @return {string} The translated text.
|
|
543
|
+
*/
|
|
544
|
+
tl(text) {
|
|
545
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
546
|
+
},
|
|
547
|
+
|
|
529
548
|
/**
|
|
530
549
|
* Replace `{value}` in a text by the value the predicate read, once
|
|
531
550
|
* one is known.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
CnBrandBar — the brand block in Nextcloud's own header (screens-brand-block-top-bar).
|
|
3
|
+
|
|
4
|
+
Draws the 237px brand block (emblem, organisation, app name, and a 1px
|
|
5
|
+
divider at its end) at the START of `#header`, before the app menu, under the
|
|
6
|
+
board look. CnAppRoot mounts it when a brand is declared and `#header` exists.
|
|
7
|
+
|
|
8
|
+
`#header` is Nextcloud's and offers no extension point for apps, so this is
|
|
9
|
+
the one place the library reaches into it, and it does so defensively:
|
|
10
|
+
- it adds ONE node of its own (a host element) as the first child of the
|
|
11
|
+
header, and teleports the block into it; it never moves, edits or removes
|
|
12
|
+
a node Nextcloud drew;
|
|
13
|
+
- it re-inserts its own host when Nextcloud re-renders the header (a
|
|
14
|
+
MutationObserver on the header and its parent), never a second one;
|
|
15
|
+
- with no `#header` it renders nothing and emits `unavailable`, so
|
|
16
|
+
CnAppRoot lets the navigation draw the block instead.
|
|
17
|
+
|
|
18
|
+
Sizes come from the board: `dossiq/DqKop`.
|
|
19
|
+
-->
|
|
20
|
+
<template>
|
|
21
|
+
<Teleport v-if="host" :to="host">
|
|
22
|
+
<div class="cn-brand-bar" data-testid="cn-brand-bar">
|
|
23
|
+
<div class="cn-brand-bar__brand" data-testid="cn-brand-bar-brand">
|
|
24
|
+
<!-- Decorative beside a name: the name already says whose app this is. -->
|
|
25
|
+
<img
|
|
26
|
+
v-if="typeof brand.emblem === 'string' && brand.emblem !== ''"
|
|
27
|
+
class="cn-brand-bar__emblem"
|
|
28
|
+
data-testid="cn-brand-bar-emblem"
|
|
29
|
+
:src="brand.emblem"
|
|
30
|
+
:alt="brand.name ? '' : brand.alt">
|
|
31
|
+
<span
|
|
32
|
+
v-else-if="brand.emblem === true"
|
|
33
|
+
class="cn-brand-bar__emblem cn-brand-bar__emblem--theme"
|
|
34
|
+
data-testid="cn-brand-bar-emblem"
|
|
35
|
+
aria-hidden="true" />
|
|
36
|
+
<img
|
|
37
|
+
v-else-if="brand.logo"
|
|
38
|
+
class="cn-brand-bar__emblem"
|
|
39
|
+
:src="brand.logo"
|
|
40
|
+
:alt="brand.name ? '' : brand.alt">
|
|
41
|
+
<span v-if="brand.name || brand.caption" class="cn-brand-bar__text">
|
|
42
|
+
<span v-if="brand.caption" class="cn-brand-bar__caption">{{ brand.caption }}</span>
|
|
43
|
+
<strong v-if="brand.name" class="cn-brand-bar__name">{{ brand.name }}</strong>
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
<span class="cn-brand-bar__divider" aria-hidden="true" />
|
|
47
|
+
</div>
|
|
48
|
+
</Teleport>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script>
|
|
52
|
+
import { markRaw } from 'vue'
|
|
53
|
+
|
|
54
|
+
const HOST_CLASS = 'cn-brand-bar-host'
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The brand block in Nextcloud's header.
|
|
58
|
+
*
|
|
59
|
+
* @event unavailable Emitted when there is no `#header` to put the block in (or it disappears), so the host lets the navigation draw it.
|
|
60
|
+
*
|
|
61
|
+
* @spec openspec/changes/screens-brand-block-top-bar/specs/layout-components/spec.md#requirement-the-board-look-draws-the-brand-block-in-the-nextcloud-header
|
|
62
|
+
*/
|
|
63
|
+
export default {
|
|
64
|
+
name: 'CnBrandBar',
|
|
65
|
+
|
|
66
|
+
props: {
|
|
67
|
+
/**
|
|
68
|
+
* The resolved brand: `{ logo, emblem, name, caption, alt }`. `emblem`
|
|
69
|
+
* is a URL, or `true` for the theme's `--nldesign-emblem-url`.
|
|
70
|
+
*
|
|
71
|
+
* @type {{logo?: string, emblem?: (string|boolean), name?: string, caption?: string, alt?: string}}
|
|
72
|
+
*/
|
|
73
|
+
brand: {
|
|
74
|
+
type: Object,
|
|
75
|
+
required: true,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
emits: ['unavailable'],
|
|
80
|
+
|
|
81
|
+
data() {
|
|
82
|
+
return { host: null, observer: null }
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
mounted() {
|
|
86
|
+
this.attach()
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
beforeUnmount() {
|
|
90
|
+
this.detach()
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
methods: {
|
|
94
|
+
/**
|
|
95
|
+
* Insert the host as the first child of `#header` and watch the header.
|
|
96
|
+
* Emits `unavailable` when there is no header.
|
|
97
|
+
*
|
|
98
|
+
* @return {void}
|
|
99
|
+
*/
|
|
100
|
+
attach() {
|
|
101
|
+
const header = document.getElementById('header')
|
|
102
|
+
if (!header) {
|
|
103
|
+
/**
|
|
104
|
+
* @event unavailable Emitted when there is no `#header` to put the block in.
|
|
105
|
+
*/
|
|
106
|
+
this.$emit('unavailable')
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
this.host = markRaw(document.createElement('div'))
|
|
110
|
+
this.host.className = HOST_CLASS
|
|
111
|
+
header.insertBefore(this.host, header.firstChild)
|
|
112
|
+
if (typeof MutationObserver === 'function') {
|
|
113
|
+
this.observer = new MutationObserver(() => this.reattach())
|
|
114
|
+
this.observer.observe(header, { childList: true })
|
|
115
|
+
if (header.parentNode) {
|
|
116
|
+
this.observer.observe(header.parentNode, { childList: true })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Put the same host back when Nextcloud re-rendered the header and took
|
|
123
|
+
* it out. Never creates a second host; when the header is gone for good
|
|
124
|
+
* it hands the block back to the navigation.
|
|
125
|
+
*
|
|
126
|
+
* @return {void}
|
|
127
|
+
*/
|
|
128
|
+
reattach() {
|
|
129
|
+
if (!this.host) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
const header = document.getElementById('header')
|
|
133
|
+
if (!header) {
|
|
134
|
+
this.$emit('unavailable')
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if (this.host.parentNode !== header) {
|
|
138
|
+
header.insertBefore(this.host, header.firstChild)
|
|
139
|
+
this.observer?.observe(header, { childList: true })
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Stop watching and remove only the host this component added.
|
|
145
|
+
*
|
|
146
|
+
* @return {void}
|
|
147
|
+
*/
|
|
148
|
+
detach() {
|
|
149
|
+
this.observer?.disconnect()
|
|
150
|
+
this.observer = null
|
|
151
|
+
this.host?.remove()
|
|
152
|
+
this.host = null
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
</script>
|
|
157
|
+
|
|
158
|
+
<style>
|
|
159
|
+
/* The host is one flex item at the start of Nextcloud's header. */
|
|
160
|
+
.cn-brand-bar-host {
|
|
161
|
+
flex: none;
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
height: 100%;
|
|
165
|
+
margin-inline-end: 12px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.cn-brand-bar {
|
|
169
|
+
box-sizing: border-box;
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: 12px;
|
|
173
|
+
width: var(--cn-board-brand-width, 237px);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.cn-brand-bar__brand {
|
|
177
|
+
flex: 1 1 0;
|
|
178
|
+
box-sizing: border-box;
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 10px;
|
|
182
|
+
min-width: 0;
|
|
183
|
+
min-height: 48px;
|
|
184
|
+
max-height: 100%;
|
|
185
|
+
padding: 0 6px;
|
|
186
|
+
border-radius: 8px;
|
|
187
|
+
color: var(--color-background-plain-text, var(--color-main-text));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.cn-brand-bar__emblem {
|
|
191
|
+
flex: none;
|
|
192
|
+
height: var(--cn-nav-emblem-size, 34px);
|
|
193
|
+
width: auto;
|
|
194
|
+
object-fit: contain;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.cn-brand-bar__emblem--theme {
|
|
198
|
+
display: inline-block;
|
|
199
|
+
width: var(--cn-nav-emblem-size, 34px);
|
|
200
|
+
background: var(--nldesign-emblem-url) center / contain no-repeat;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.cn-brand-bar__text {
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-direction: column;
|
|
206
|
+
line-height: 1.15;
|
|
207
|
+
min-width: 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.cn-brand-bar__caption {
|
|
211
|
+
font-size: 13px;
|
|
212
|
+
white-space: nowrap;
|
|
213
|
+
opacity: 0.8;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.cn-brand-bar__name {
|
|
217
|
+
font-size: 18px;
|
|
218
|
+
font-weight: 700;
|
|
219
|
+
white-space: nowrap;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.cn-brand-bar__divider {
|
|
223
|
+
flex: none;
|
|
224
|
+
width: 1px;
|
|
225
|
+
height: 28px;
|
|
226
|
+
background: currentColor;
|
|
227
|
+
opacity: 0.25;
|
|
228
|
+
color: var(--color-background-plain-text, var(--color-main-text));
|
|
229
|
+
}
|
|
230
|
+
</style>
|
|
@@ -103,7 +103,10 @@ export default {
|
|
|
103
103
|
return
|
|
104
104
|
}
|
|
105
105
|
const all = Object.values(ENVIRONMENT_TITLE_PREFIX)
|
|
106
|
-
|
|
106
|
+
// Plain string comparison: no pattern is built from the prefixes, so
|
|
107
|
+
// there is nothing to escape.
|
|
108
|
+
const owned = all.find((p) => document.title.startsWith(`${p} `))
|
|
109
|
+
const bare = owned ? document.title.slice(owned.length + 1) : document.title
|
|
107
110
|
const wanted = environment ? `${ENVIRONMENT_TITLE_PREFIX[environment]} ${bare}` : bare
|
|
108
111
|
if (document.title !== wanted) {
|
|
109
112
|
document.title = wanted
|
|
@@ -309,6 +309,7 @@
|
|
|
309
309
|
and select machinery is unchanged either way. -->
|
|
310
310
|
<component
|
|
311
311
|
:is="isUserField(field) ? 'NcSelectUsers' : 'NcSelect'"
|
|
312
|
+
v-cn-select-aria="fieldAria(field)"
|
|
312
313
|
:inputId="'cn-form-' + field.key"
|
|
313
314
|
:inputLabel="fieldLabelText(field)"
|
|
314
315
|
:labelOutside="isBoardLook"
|
|
@@ -358,6 +359,7 @@
|
|
|
358
359
|
<!-- Multiselect (array enum items / $ref array / Nextcloud users, supports async function) -->
|
|
359
360
|
<div v-else-if="field.widget === 'multiselect' || field.widget === 'user-multiselect' || field.widget === 'group-multiselect'" class="cn-form-dialog__select-wrapper">
|
|
360
361
|
<NcSelect
|
|
362
|
+
v-cn-select-aria="fieldAria(field)"
|
|
361
363
|
:inputId="'cn-form-' + field.key"
|
|
362
364
|
:inputLabel="fieldLabelText(field)"
|
|
363
365
|
:labelOutside="isBoardLook"
|
|
@@ -401,6 +403,7 @@
|
|
|
401
403
|
<div v-else-if="field.widget === 'tags'" class="cn-form-dialog__select-wrapper">
|
|
402
404
|
<!-- TODO: restore `:options` to `asyncState[field.key]?.options` once on Vue 3 (buble doesn't support optional chaining) -->
|
|
403
405
|
<NcSelect
|
|
406
|
+
v-cn-select-aria="fieldAria(field)"
|
|
404
407
|
:inputId="'cn-form-' + field.key"
|
|
405
408
|
:inputLabel="fieldLabelText(field)"
|
|
406
409
|
:labelOutside="isBoardLook"
|
|
@@ -789,6 +792,7 @@ import { heldFromFailures, splitHeldFiles, uploadHeldFiles } from '../../composa
|
|
|
789
792
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
790
793
|
import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js'
|
|
791
794
|
import { useWriteFeedback } from '../../composables/useWriteFeedback.js'
|
|
795
|
+
import { cnSelectAria } from '../../directives/cnSelectAria.js'
|
|
792
796
|
import { dialogBoardMixin } from '../../mixins/dialogBoard.js'
|
|
793
797
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
794
798
|
import { resolveCreateOverrideHandler } from '../../utils/actionsDispatcher.js'
|
|
@@ -1009,6 +1013,8 @@ const SEMANTIC_RESOLVE_ENDPOINT = '/apps/openregister/api/schemas/resolve-by-imp
|
|
|
1009
1013
|
export default {
|
|
1010
1014
|
name: 'CnFormDialog',
|
|
1011
1015
|
|
|
1016
|
+
directives: { cnSelectAria },
|
|
1017
|
+
|
|
1012
1018
|
components: {
|
|
1013
1019
|
CnFileField,
|
|
1014
1020
|
CnDurationField,
|
|
@@ -3618,6 +3624,10 @@ export default {
|
|
|
3618
3624
|
return
|
|
3619
3625
|
}
|
|
3620
3626
|
const [head, ...rest] = key.split('.')
|
|
3627
|
+
const unsafe = ['__proto__', 'constructor', 'prototype']
|
|
3628
|
+
if (unsafe.includes(head) || rest.some((k) => unsafe.includes(k))) {
|
|
3629
|
+
return
|
|
3630
|
+
}
|
|
3621
3631
|
const clone = JSON.parse(JSON.stringify(this.formData[head] ?? {}))
|
|
3622
3632
|
let cur = clone
|
|
3623
3633
|
rest.slice(0, -1).forEach((k) => {
|