@conduction/nextcloud-vue 2.115.0 → 2.116.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 +92 -0
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +2 -2
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue.js +9 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue2.js +145 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue3.js +58 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChecklistWidget/index.js +28 -0
- package/dist/esm/components/CnChecklistWidget/index.js.map +1 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +376 -51
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +364 -285
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +6 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +1 -1
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +48 -1
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +11 -2
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue.js +1 -1
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js +11 -3
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js +8 -3
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +55 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +94 -4
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +248 -239
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +9 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +282 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +202 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -0
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +10 -5
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +64 -0
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useTaskInboxStore.js +85 -0
- package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js +9 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js.map +1 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js +107 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js.map +1 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js +110 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js.map +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +19 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +23 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +1054 -911
- package/dist/esm/utils/actionsDispatcher.js +10 -5
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/detailActionModel.js +6 -2
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.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/esm/utils/widgetDispatch.js +1 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +17078 -15224
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1054 -911
- package/l10n/en.json +19 -1
- package/l10n/nl.json +23 -1
- package/package.json +1 -1
- package/src/components/CnActionButtons/CnActionButtons.vue +93 -1
- package/src/components/CnChecklistWidget/CnChecklistWidget.vue +206 -0
- package/src/components/CnChecklistWidget/index.js +27 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +26 -1
- package/src/components/CnDetailPage/CnDetailPage.md +2 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +377 -10
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +15 -2
- package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +59 -1
- package/src/components/CnMacrosWidget/CnMacrosWidget.vue +16 -6
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +55 -2
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +101 -3
- package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +508 -0
- package/src/components/CnTabsWidget/CnTabsWidget.vue +10 -5
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/index.js +1 -0
- package/src/composables/indexSources.js +64 -0
- package/src/composables/useTaskInboxStore.js +85 -0
- package/src/dialogs/CnAccessPurposeDialog.vue +183 -0
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +49 -3
- package/src/utils/actionsDispatcher.js +10 -5
- package/src/utils/detailActionModel.js +6 -2
- package/src/utils/libraryWidgetKeys.js +1 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/widgetDispatch.js +1 -1
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
<!-- Breadcrumb line (manifest `config.breadcrumb`): the list the record
|
|
45
45
|
belongs to, then the record itself as the current crumb. -->
|
|
46
46
|
<CnBreadcrumbs
|
|
47
|
-
v-if="breadcrumbCrumbs.length > 0 && !objectNotFound && !isBoard"
|
|
47
|
+
v-if="breadcrumbCrumbs.length > 0 && !objectNotFound && !contentWithheld && !isBoard"
|
|
48
48
|
class="cn-detail-page__breadcrumbs"
|
|
49
49
|
:crumbs="breadcrumbCrumbs"
|
|
50
50
|
:rootText="breadcrumbCrumbs[0].icon === undefined"
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
data-testid="cn-detail-page-breadcrumbs" />
|
|
53
53
|
<!-- Header -->
|
|
54
54
|
<div
|
|
55
|
-
v-if="!objectNotFound"
|
|
55
|
+
v-if="!objectNotFound && !contentWithheld"
|
|
56
56
|
class="cn-detail-page__header"
|
|
57
57
|
:class="{ 'cn-detail-page__header--card': headerCard && !isBoard, 'cn-detail-page__header--with-widget': headerWidgetDef !== null, 'cn-detail-page__header--board': isBoard }"
|
|
58
58
|
data-testid="cn-detail-page-header">
|
|
@@ -264,6 +264,22 @@
|
|
|
264
264
|
@click="onPrimaryActionClick">
|
|
265
265
|
{{ effectivePrimaryLabel }}
|
|
266
266
|
</NcButton>
|
|
267
|
+
<!-- The record source's own verbs (a task's claim, hand back,
|
|
268
|
+
complete), offered from the record's state. The ones a source
|
|
269
|
+
marks `menu` join the Actions menu instead. -->
|
|
270
|
+
<NcButton
|
|
271
|
+
v-for="verb in sourceVerbButtons"
|
|
272
|
+
:key="`verb-${verb.id}`"
|
|
273
|
+
variant="secondary"
|
|
274
|
+
:disabled="sourceVerbBusy"
|
|
275
|
+
:data-testid="`cn-detail-page-verb-${verb.id}`"
|
|
276
|
+
@click="runSourceVerb(verb)">
|
|
277
|
+
<template v-if="verb.icon" #icon>
|
|
278
|
+
<CnIcon v-if="typeof verb.icon === 'string'" :name="verb.icon" :size="20" />
|
|
279
|
+
<component :is="verb.icon" v-else :size="20" />
|
|
280
|
+
</template>
|
|
281
|
+
{{ verb.label }}
|
|
282
|
+
</NcButton>
|
|
267
283
|
<!-- Quick actions (manifest `config.quickActions`): the few
|
|
268
284
|
actions a handler reaches for on every record, always
|
|
269
285
|
visible. At most three, so the row cannot grow back into
|
|
@@ -659,6 +675,32 @@
|
|
|
659
675
|
</slot>
|
|
660
676
|
</div>
|
|
661
677
|
|
|
678
|
+
<!-- Access purpose (`accessPurpose`): nothing of the record shows until
|
|
679
|
+
the reader has stated why they open it and the purpose is recorded. -->
|
|
680
|
+
<div v-else-if="contentWithheld" class="cn-detail-page__access-purpose" data-testid="cn-detail-page-access-purpose">
|
|
681
|
+
<CnAccessPurposeDialog
|
|
682
|
+
v-if="accessPurposeAsking"
|
|
683
|
+
:title="accessPurposeText('title')"
|
|
684
|
+
:description="accessPurposeText('description')"
|
|
685
|
+
:options="accessPurposeOptions"
|
|
686
|
+
:loading="accessPurposeBusy"
|
|
687
|
+
:error="accessPurposeError"
|
|
688
|
+
@submit="recordAccessPurpose"
|
|
689
|
+
@cancel="accessPurposeAsking = false" />
|
|
690
|
+
<NcEmptyContent
|
|
691
|
+
v-else-if="accessPurposeRequired"
|
|
692
|
+
:name="t('nextcloud-vue', 'State a purpose to open this')"
|
|
693
|
+
:description="t('nextcloud-vue', 'This record holds special categories of personal data. It opens once your purpose is recorded.')"
|
|
694
|
+
data-testid="cn-detail-page-access-purpose-refused">
|
|
695
|
+
<template #action>
|
|
696
|
+
<NcButton variant="primary" data-testid="cn-detail-page-access-purpose-ask" @click="accessPurposeAsking = true">
|
|
697
|
+
{{ t('nextcloud-vue', 'State a purpose') }}
|
|
698
|
+
</NcButton>
|
|
699
|
+
</template>
|
|
700
|
+
</NcEmptyContent>
|
|
701
|
+
<NcLoadingIcon v-else :size="32" :name="loadingLabel" />
|
|
702
|
+
</div>
|
|
703
|
+
|
|
662
704
|
<!-- Loading state -->
|
|
663
705
|
<div v-else-if="showLoadingState" class="cn-detail-page__loading">
|
|
664
706
|
<NcLoadingIcon :size="32" />
|
|
@@ -1231,6 +1273,7 @@ import Plus from 'vue-material-design-icons/Plus.vue'
|
|
|
1231
1273
|
import Refresh from 'vue-material-design-icons/Refresh.vue'
|
|
1232
1274
|
import Restore from 'vue-material-design-icons/Restore.vue'
|
|
1233
1275
|
import ViewDashboardOutline from 'vue-material-design-icons/ViewDashboardOutline.vue'
|
|
1276
|
+
import CnAccessPurposeDialog from '../../dialogs/CnAccessPurposeDialog.vue'
|
|
1234
1277
|
import CnRelationLinkModal from '../../dialogs/CnRelationLinkModal.vue'
|
|
1235
1278
|
import CnWidgetStyleEditorModal from '../../dialogs/CnWidgetStyleEditorModal.vue'
|
|
1236
1279
|
import CnActionsMenu from '../CnActionsMenu/CnActionsMenu.vue'
|
|
@@ -1280,6 +1323,7 @@ import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
|
|
|
1280
1323
|
import { firstFilled } from '../../utils/fillTemplate.js'
|
|
1281
1324
|
import { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'
|
|
1282
1325
|
import { isActivityWidget, resolveHeaderMeta } from '../../utils/headerMeta.js'
|
|
1326
|
+
import { prefixUrl } from '../../utils/headers.js'
|
|
1283
1327
|
import { patchStoredSelf } from '../../utils/patchStoredSelf.js'
|
|
1284
1328
|
import { setReadState } from '../../utils/recordInteractions.js'
|
|
1285
1329
|
import { saveEdited } from '../../utils/saveEdited.js'
|
|
@@ -1374,6 +1418,25 @@ const PAGE_REFRESH_CHANNEL = 'cn:page:refresh'
|
|
|
1374
1418
|
/** Surfaces understood by the pluggable integration registry (AD-19). */
|
|
1375
1419
|
const INTEGRATION_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']
|
|
1376
1420
|
|
|
1421
|
+
/** The session storage key that lists the records whose access purpose is recorded. */
|
|
1422
|
+
const ACCESS_PURPOSE_SESSION_KEY = 'cn-access-purposes'
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* The records whose access purpose this browser session recorded. Empty when
|
|
1426
|
+
* session storage cannot be read (a private window, blocked site data).
|
|
1427
|
+
*
|
|
1428
|
+
* @return {Array<string>} The record keys.
|
|
1429
|
+
*/
|
|
1430
|
+
function readGrantedPurposes() {
|
|
1431
|
+
try {
|
|
1432
|
+
const raw = window.sessionStorage.getItem(ACCESS_PURPOSE_SESSION_KEY)
|
|
1433
|
+
const list = raw ? JSON.parse(raw) : []
|
|
1434
|
+
return Array.isArray(list) ? list.filter((key) => typeof key === 'string') : []
|
|
1435
|
+
} catch {
|
|
1436
|
+
return []
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1377
1440
|
/**
|
|
1378
1441
|
* CnDetailPage — Generic detail/overview page.
|
|
1379
1442
|
*
|
|
@@ -1450,6 +1513,7 @@ export default {
|
|
|
1450
1513
|
|
|
1451
1514
|
components: {
|
|
1452
1515
|
ChevronLeft,
|
|
1516
|
+
CnAccessPurposeDialog,
|
|
1453
1517
|
ChevronRight,
|
|
1454
1518
|
NcActionButton,
|
|
1455
1519
|
NcActionCaption,
|
|
@@ -1547,6 +1611,7 @@ export default {
|
|
|
1547
1611
|
const isBoard = computed(() => this.isBoard)
|
|
1548
1612
|
const tabsLabel = computed(() => this.resolvedTabsLabel)
|
|
1549
1613
|
const visibilityRows = computed(() => (this.visibilityRowsPending ? null : this.visibilityRows))
|
|
1614
|
+
const canCheckItems = computed(() => this.recordCanCheckItems)
|
|
1550
1615
|
return {
|
|
1551
1616
|
// The page's own look reaches the tabs, the cards and the activity
|
|
1552
1617
|
// below it, also when it came from the `look` prop and not the app.
|
|
@@ -1557,6 +1622,15 @@ export default {
|
|
|
1557
1622
|
},
|
|
1558
1623
|
},
|
|
1559
1624
|
|
|
1625
|
+
// The record source's checklist writes, for a `checklist` widget.
|
|
1626
|
+
cnRecordChecklist: {
|
|
1627
|
+
get canCheck() {
|
|
1628
|
+
return canCheckItems.value
|
|
1629
|
+
},
|
|
1630
|
+
|
|
1631
|
+
check: (itemId, checked) => this.checkSourceItem(itemId, checked),
|
|
1632
|
+
},
|
|
1633
|
+
|
|
1560
1634
|
// The open body tab, reported by a `tabs` widget, so a side entry
|
|
1561
1635
|
// with `tabs` shows only beside those tabs.
|
|
1562
1636
|
cnDetailReportTab: (id) => {
|
|
@@ -1954,6 +2028,20 @@ export default {
|
|
|
1954
2028
|
default: '',
|
|
1955
2029
|
},
|
|
1956
2030
|
|
|
2031
|
+
/**
|
|
2032
|
+
* App routes that replace a record source's own route for a verb, by
|
|
2033
|
+
* verb id (manifest `config.verbEndpoints`), such as
|
|
2034
|
+
* `{ "complete": "/apps/dossiq/api/case-tasks/{id}/complete" }` for an
|
|
2035
|
+
* app that checks something of its own before the engine completes the
|
|
2036
|
+
* task. `{id}` is the record id. Only read with `entitySource`.
|
|
2037
|
+
*
|
|
2038
|
+
* @type {object|null}
|
|
2039
|
+
*/
|
|
2040
|
+
verbEndpoints: {
|
|
2041
|
+
type: Object,
|
|
2042
|
+
default: null,
|
|
2043
|
+
},
|
|
2044
|
+
|
|
1957
2045
|
/**
|
|
1958
2046
|
* OpenRegister register slug. Pair with `schema` to opt into the
|
|
1959
2047
|
* schema-driven mode: the page fuses the two into an internal
|
|
@@ -2517,9 +2605,11 @@ export default {
|
|
|
2517
2605
|
* shows nothing at 0. With `resolve: { register, schema, labelField?,
|
|
2518
2606
|
* colorField?, iconField? }` the pill shows the referenced record's
|
|
2519
2607
|
* label, takes its tone from that record's colour (an NL Design System
|
|
2520
|
-
* hue such as `red`, or a badge role) and shows its icon.
|
|
2608
|
+
* hue such as `red`, or a badge role) and shows its icon. `values`
|
|
2609
|
+
* draws a `field` pill only for those raw values (a priority pill for
|
|
2610
|
+
* `high` and `urgent` only).
|
|
2521
2611
|
*
|
|
2522
|
-
* @type {{field?: string, text?: string, countFrom?: object, countLabel?: object, colorMap?: object, labels?: object, variant?: string, resolve?: object}|null}
|
|
2612
|
+
* @type {{field?: string, text?: string, countFrom?: object, countLabel?: object, colorMap?: object, labels?: object, variant?: string, resolve?: object, values?: Array<string>}|null}
|
|
2523
2613
|
*/
|
|
2524
2614
|
typePill: {
|
|
2525
2615
|
type: Object,
|
|
@@ -2697,6 +2787,28 @@ export default {
|
|
|
2697
2787
|
default: () => ({}),
|
|
2698
2788
|
},
|
|
2699
2789
|
|
|
2790
|
+
/**
|
|
2791
|
+
* Ask why the reader opens the record before the page shows it, and
|
|
2792
|
+
* record the answer (manifest `config.accessPurpose`). `{ when, options,
|
|
2793
|
+
* url, title?, description? }`: `when` is a condition in the
|
|
2794
|
+
* `visibleWhen` grammar on the record and its `visibilityContext`
|
|
2795
|
+
* records (DqSociaalDomein: the case type lists special categories of
|
|
2796
|
+
* personal data); `options` the purposes offered (`{ value, label }`,
|
|
2797
|
+
* "Other" with a written purpose is always added); `url` the app
|
|
2798
|
+
* endpoint that records the purpose (POST `{ purpose, note }`, `{objectId}`
|
|
2799
|
+
* filled in). The page shows nothing of the record until the endpoint
|
|
2800
|
+
* accepts, and does not ask again for the same record in the same
|
|
2801
|
+
* browser session. A refusal keeps the record closed. `null` (the
|
|
2802
|
+
* default) asks nothing.
|
|
2803
|
+
*
|
|
2804
|
+
* @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
|
|
2805
|
+
* @type {object|null}
|
|
2806
|
+
*/
|
|
2807
|
+
accessPurpose: {
|
|
2808
|
+
type: Object,
|
|
2809
|
+
default: null,
|
|
2810
|
+
},
|
|
2811
|
+
|
|
2700
2812
|
/**
|
|
2701
2813
|
* Whether the current user administers this instance. Shows the
|
|
2702
2814
|
* `adminOnly` header actions. `null` (the default) reads it from
|
|
@@ -2939,6 +3051,14 @@ export default {
|
|
|
2939
3051
|
|
|
2940
3052
|
data() {
|
|
2941
3053
|
return {
|
|
3054
|
+
/** The session keys of the records whose access purpose is recorded. */
|
|
3055
|
+
accessPurposeGranted: readGrantedPurposes(),
|
|
3056
|
+
/** Whether the purpose dialog is open; closing it without a purpose keeps the record closed. */
|
|
3057
|
+
accessPurposeAsking: true,
|
|
3058
|
+
/** Whether a purpose is being recorded. */
|
|
3059
|
+
accessPurposeBusy: false,
|
|
3060
|
+
/** Why the last purpose was refused. */
|
|
3061
|
+
accessPurposeError: '',
|
|
2942
3062
|
/** The id of the open body tab a `tabs` widget reported, '' before one did. */
|
|
2943
3063
|
activeBodyTab: '',
|
|
2944
3064
|
/**
|
|
@@ -2989,6 +3109,8 @@ export default {
|
|
|
2989
3109
|
* menu dispatches without reaching back into that component.
|
|
2990
3110
|
*/
|
|
2991
3111
|
menuHeaderActions: [],
|
|
3112
|
+
/** Whether a record-source verb is in flight, so a double click cannot run it twice. */
|
|
3113
|
+
sourceVerbBusy: false,
|
|
2992
3114
|
/** Lifecycle transitions CnLifecycleActions hands the Actions menu. */
|
|
2993
3115
|
lifecycleMenuEntries: [],
|
|
2994
3116
|
/** Whether the per-widget style/config editor modal is open. */
|
|
@@ -3440,7 +3562,64 @@ export default {
|
|
|
3440
3562
|
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card
|
|
3441
3563
|
*/
|
|
3442
3564
|
showsBody() {
|
|
3443
|
-
return !this.objectNotFound && !this.showLoadingState && !this.error && !this.empty
|
|
3565
|
+
return !this.objectNotFound && !this.contentWithheld && !this.showLoadingState && !this.error && !this.empty
|
|
3566
|
+
},
|
|
3567
|
+
|
|
3568
|
+
/**
|
|
3569
|
+
* Whether the loaded record asks for a purpose before it opens
|
|
3570
|
+
* (`accessPurpose.when` holds for it). Undecided, and so withheld,
|
|
3571
|
+
* while the record or the records its condition reads are on their way.
|
|
3572
|
+
*
|
|
3573
|
+
* @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
|
|
3574
|
+
* @return {boolean}
|
|
3575
|
+
*/
|
|
3576
|
+
accessPurposeRequired() {
|
|
3577
|
+
const gate = this.accessPurpose
|
|
3578
|
+
if (!gate || typeof gate !== 'object' || !gate.when || !this.widgetVisibilityData || this.visibilityRowsPending) {
|
|
3579
|
+
return false
|
|
3580
|
+
}
|
|
3581
|
+
return detailWidgetVisible({ visibleWhen: gate.when }, this.widgetVisibilityData)
|
|
3582
|
+
},
|
|
3583
|
+
|
|
3584
|
+
/**
|
|
3585
|
+
* Whether the page holds the record back: it asks for a purpose and
|
|
3586
|
+
* none is recorded for this record in this session yet, or it cannot
|
|
3587
|
+
* tell yet whether it asks.
|
|
3588
|
+
*
|
|
3589
|
+
* @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
|
|
3590
|
+
* @return {boolean}
|
|
3591
|
+
*/
|
|
3592
|
+
contentWithheld() {
|
|
3593
|
+
const gate = this.accessPurpose
|
|
3594
|
+
if (!gate || typeof gate !== 'object' || !gate.when || this.objectNotFound) {
|
|
3595
|
+
return false
|
|
3596
|
+
}
|
|
3597
|
+
if (this.accessPurposeGranted.includes(this.accessPurposeKey)) {
|
|
3598
|
+
return false
|
|
3599
|
+
}
|
|
3600
|
+
const undecided = !this.widgetVisibilityData || this.visibilityRowsPending
|
|
3601
|
+
return undecided || this.accessPurposeRequired
|
|
3602
|
+
},
|
|
3603
|
+
|
|
3604
|
+
/**
|
|
3605
|
+
* The session key of the purpose recorded for this record.
|
|
3606
|
+
*
|
|
3607
|
+
* @return {string}
|
|
3608
|
+
*/
|
|
3609
|
+
accessPurposeKey() {
|
|
3610
|
+
return `cn-access-purpose:${this.resolvedObjectType || ''}:${this.objectId || ''}`
|
|
3611
|
+
},
|
|
3612
|
+
|
|
3613
|
+
/**
|
|
3614
|
+
* The purposes offered, through the app's translate.
|
|
3615
|
+
*
|
|
3616
|
+
* @return {Array<{value: string, label: string}>}
|
|
3617
|
+
*/
|
|
3618
|
+
accessPurposeOptions() {
|
|
3619
|
+
const options = Array.isArray(this.accessPurpose?.options) ? this.accessPurpose.options : []
|
|
3620
|
+
return options
|
|
3621
|
+
.filter((option) => option && typeof option.value === 'string' && option.value !== '')
|
|
3622
|
+
.map((option) => ({ value: option.value, label: this.cnTranslate(option.label || option.value) }))
|
|
3444
3623
|
},
|
|
3445
3624
|
|
|
3446
3625
|
/**
|
|
@@ -3929,7 +4108,70 @@ export default {
|
|
|
3929
4108
|
const inMenu = pinned.size > 0
|
|
3930
4109
|
? this.menuHeaderActions.filter((entry) => !pinned.has(entry.id))
|
|
3931
4110
|
: this.menuHeaderActions
|
|
3932
|
-
return [...lifecycle, ...inMenu]
|
|
4111
|
+
return [...this.sourceVerbMenuEntries, ...lifecycle, ...inMenu]
|
|
4112
|
+
},
|
|
4113
|
+
|
|
4114
|
+
/**
|
|
4115
|
+
* The verbs the record source offers on the record in view, read off
|
|
4116
|
+
* the record's own state by the source. Empty without a source that
|
|
4117
|
+
* offers verbs, before the record is read, and when it was not found.
|
|
4118
|
+
*
|
|
4119
|
+
* @return {Array<object>} The verbs.
|
|
4120
|
+
* @spec openspec/changes/record-source-verbs/specs/record-source-verbs/spec.md#requirement-a-record-source-offers-header-verbs
|
|
4121
|
+
*/
|
|
4122
|
+
sourceVerbs() {
|
|
4123
|
+
const source = this.recordSource
|
|
4124
|
+
if (!source || typeof source.verbs !== 'function' || typeof source.runVerb !== 'function') {
|
|
4125
|
+
return []
|
|
4126
|
+
}
|
|
4127
|
+
if (this.objectNotFound || !this.currentObject) {
|
|
4128
|
+
return []
|
|
4129
|
+
}
|
|
4130
|
+
const verbs = source.verbs(this.currentObject)
|
|
4131
|
+
return Array.isArray(verbs) ? verbs.filter((verb) => verb && typeof verb.id === 'string' && verb.id !== '') : []
|
|
4132
|
+
},
|
|
4133
|
+
|
|
4134
|
+
/**
|
|
4135
|
+
* Whether the record source can tick the checklist of the record in
|
|
4136
|
+
* view: it writes items, and says this record can still be ticked.
|
|
4137
|
+
*
|
|
4138
|
+
* @return {boolean}
|
|
4139
|
+
* @spec openspec/changes/record-checklist-widget/specs/record-checklist-widget/spec.md#requirement-a-checklist-widget-ticks-items-through-the-record-source
|
|
4140
|
+
*/
|
|
4141
|
+
recordCanCheckItems() {
|
|
4142
|
+
const source = this.recordSource
|
|
4143
|
+
if (!source || typeof source.checkItem !== 'function' || !this.currentObject || this.objectNotFound) {
|
|
4144
|
+
return false
|
|
4145
|
+
}
|
|
4146
|
+
return typeof source.canCheckItems !== 'function' || source.canCheckItems(this.currentObject) === true
|
|
4147
|
+
},
|
|
4148
|
+
|
|
4149
|
+
/**
|
|
4150
|
+
* The source verbs drawn as header buttons.
|
|
4151
|
+
*
|
|
4152
|
+
* @return {Array<object>} The verbs.
|
|
4153
|
+
*/
|
|
4154
|
+
sourceVerbButtons() {
|
|
4155
|
+
return this.sourceVerbs.filter((verb) => verb.menu !== true)
|
|
4156
|
+
},
|
|
4157
|
+
|
|
4158
|
+
/**
|
|
4159
|
+
* The source verbs listed in the Actions menu, as menu entries.
|
|
4160
|
+
*
|
|
4161
|
+
* @return {Array<object>} Menu-ready entries.
|
|
4162
|
+
*/
|
|
4163
|
+
sourceVerbMenuEntries() {
|
|
4164
|
+
return this.sourceVerbs
|
|
4165
|
+
.filter((verb) => verb.menu === true)
|
|
4166
|
+
.map((verb) => ({
|
|
4167
|
+
id: `cn-source-verb-${verb.id}`,
|
|
4168
|
+
label: verb.label,
|
|
4169
|
+
testid: `cn-detail-page-verb-${verb.id}`,
|
|
4170
|
+
iconName: typeof verb.icon === 'string' ? verb.icon : '',
|
|
4171
|
+
disabled: this.sourceVerbBusy,
|
|
4172
|
+
pressed: null,
|
|
4173
|
+
run: () => this.runSourceVerb(verb),
|
|
4174
|
+
}))
|
|
3933
4175
|
},
|
|
3934
4176
|
|
|
3935
4177
|
/**
|
|
@@ -4919,6 +5161,16 @@ export default {
|
|
|
4919
5161
|
this.syncSidebarState()
|
|
4920
5162
|
},
|
|
4921
5163
|
|
|
5164
|
+
contentWithheld() {
|
|
5165
|
+
this.syncSidebarState()
|
|
5166
|
+
},
|
|
5167
|
+
|
|
5168
|
+
/** Another record asks its own purpose: the dialog opens again for it. */
|
|
5169
|
+
accessPurposeKey() {
|
|
5170
|
+
this.accessPurposeAsking = true
|
|
5171
|
+
this.accessPurposeError = ''
|
|
5172
|
+
},
|
|
5173
|
+
|
|
4922
5174
|
// Deep, like `sidebarTabs` / `sidebarProps` below: the manifest editors
|
|
4923
5175
|
// mutate `page.config.sidebar` IN PLACE, so the reference CnPageRenderer
|
|
4924
5176
|
// forwards never changes. A shallow watcher missed every such edit —
|
|
@@ -5095,6 +5347,52 @@ export default {
|
|
|
5095
5347
|
},
|
|
5096
5348
|
|
|
5097
5349
|
methods: {
|
|
5350
|
+
/**
|
|
5351
|
+
* A text of the access purpose gate through the app's translate, '' when unset.
|
|
5352
|
+
*
|
|
5353
|
+
* @param {string} key `title` or `description`.
|
|
5354
|
+
* @return {string}
|
|
5355
|
+
*/
|
|
5356
|
+
accessPurposeText(key) {
|
|
5357
|
+
const text = this.accessPurpose && this.accessPurpose[key]
|
|
5358
|
+
return typeof text === 'string' && text !== '' ? this.cnTranslate(text) : ''
|
|
5359
|
+
},
|
|
5360
|
+
|
|
5361
|
+
/**
|
|
5362
|
+
* Record the reader's purpose with the app's endpoint, then open the
|
|
5363
|
+
* record and remember it for this browser session. A refusal (or a
|
|
5364
|
+
* failure to record) keeps the record closed and says why.
|
|
5365
|
+
*
|
|
5366
|
+
* @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
|
|
5367
|
+
* @param {{purpose: string, note: string}} answer The chosen purpose.
|
|
5368
|
+
* @return {Promise<void>}
|
|
5369
|
+
*/
|
|
5370
|
+
async recordAccessPurpose(answer) {
|
|
5371
|
+
const url = String(this.accessPurpose?.url || '').replace(/\{objectId\}/g, encodeURIComponent(String(this.objectId || '')))
|
|
5372
|
+
if (url === '') {
|
|
5373
|
+
this.accessPurposeError = t('nextcloud-vue', 'This page cannot record a purpose, so the record stays closed.')
|
|
5374
|
+
return
|
|
5375
|
+
}
|
|
5376
|
+
this.accessPurposeBusy = true
|
|
5377
|
+
this.accessPurposeError = ''
|
|
5378
|
+
try {
|
|
5379
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
5380
|
+
await axios.post(prefixUrl(url), { purpose: answer.purpose, note: answer.note })
|
|
5381
|
+
const key = this.accessPurposeKey
|
|
5382
|
+
this.accessPurposeGranted = [...new Set([...this.accessPurposeGranted, key])]
|
|
5383
|
+
try {
|
|
5384
|
+
window.sessionStorage.setItem(ACCESS_PURPOSE_SESSION_KEY, JSON.stringify(this.accessPurposeGranted))
|
|
5385
|
+
} catch {
|
|
5386
|
+
// Without session storage the purpose is asked again on the next visit.
|
|
5387
|
+
}
|
|
5388
|
+
} catch (error) {
|
|
5389
|
+
const data = error?.response?.data
|
|
5390
|
+
this.accessPurposeError = (data && (data.message || data.error)) || t('nextcloud-vue', 'Your purpose could not be recorded, so the record stays closed.')
|
|
5391
|
+
} finally {
|
|
5392
|
+
this.accessPurposeBusy = false
|
|
5393
|
+
}
|
|
5394
|
+
},
|
|
5395
|
+
|
|
5098
5396
|
/**
|
|
5099
5397
|
* The widget definitions with each `tabs` widget's tabs narrowed and
|
|
5100
5398
|
* ordered by `pickedTabIds`; the definitions as given without a pick.
|
|
@@ -5552,6 +5850,65 @@ export default {
|
|
|
5552
5850
|
this.$emit('transitioned', payload)
|
|
5553
5851
|
},
|
|
5554
5852
|
|
|
5853
|
+
/**
|
|
5854
|
+
* Tick or untick one checklist item through the record source. The
|
|
5855
|
+
* source replaces its record with the answer; a refusal is shown with
|
|
5856
|
+
* the server's message and leaves the item as it was.
|
|
5857
|
+
*
|
|
5858
|
+
* @param {string} itemId The item id.
|
|
5859
|
+
* @param {boolean} checked The new state.
|
|
5860
|
+
* @return {Promise<boolean>} Whether the write was taken.
|
|
5861
|
+
* @spec openspec/changes/record-checklist-widget/specs/record-checklist-widget/spec.md#requirement-a-checklist-widget-ticks-items-through-the-record-source
|
|
5862
|
+
*/
|
|
5863
|
+
async checkSourceItem(itemId, checked) {
|
|
5864
|
+
const source = this.recordSource
|
|
5865
|
+
const objectId = this.objectId
|
|
5866
|
+
if (!this.recordCanCheckItems || !objectId) {
|
|
5867
|
+
return false
|
|
5868
|
+
}
|
|
5869
|
+
const outcome = await source.checkItem(String(objectId), String(itemId), checked === true)
|
|
5870
|
+
if (!outcome || outcome.ok !== true) {
|
|
5871
|
+
const { showError } = await import('@nextcloud/dialogs')
|
|
5872
|
+
showError(String(outcome?.message || '').trim() || t('nextcloud-vue', 'This could not be done. Try again.'))
|
|
5873
|
+
return false
|
|
5874
|
+
}
|
|
5875
|
+
return true
|
|
5876
|
+
},
|
|
5877
|
+
|
|
5878
|
+
/**
|
|
5879
|
+
* Run a record-source verb on the record in view. The source keeps its
|
|
5880
|
+
* record in step with the answer, so the header, pills and widgets
|
|
5881
|
+
* redraw from it. A refusal is shown with the server's own message,
|
|
5882
|
+
* which names the verb and the reason.
|
|
5883
|
+
*
|
|
5884
|
+
* @param {{id: string, label: string, successMessage?: string}} verb The verb.
|
|
5885
|
+
* @return {Promise<void>}
|
|
5886
|
+
* @spec openspec/changes/record-source-verbs/specs/record-source-verbs/spec.md#requirement-a-record-source-offers-header-verbs
|
|
5887
|
+
*/
|
|
5888
|
+
async runSourceVerb(verb) {
|
|
5889
|
+
const source = this.recordSource
|
|
5890
|
+
const objectId = this.objectId
|
|
5891
|
+
if (!source || !objectId || this.sourceVerbBusy) {
|
|
5892
|
+
return
|
|
5893
|
+
}
|
|
5894
|
+
this.sourceVerbBusy = true
|
|
5895
|
+
try {
|
|
5896
|
+
const url = this.verbEndpoints && typeof this.verbEndpoints[verb.id] === 'string' ? this.verbEndpoints[verb.id] : undefined
|
|
5897
|
+
const outcome = await source.runVerb(String(objectId), verb.id, url ? { url } : {})
|
|
5898
|
+
const { showError, showSuccess } = await import('@nextcloud/dialogs')
|
|
5899
|
+
if (!outcome || outcome.ok !== true) {
|
|
5900
|
+
showError(String(outcome?.message || '').trim() || t('nextcloud-vue', 'This could not be done. Try again.'))
|
|
5901
|
+
return
|
|
5902
|
+
}
|
|
5903
|
+
if (verb.successMessage) {
|
|
5904
|
+
showSuccess(verb.successMessage)
|
|
5905
|
+
}
|
|
5906
|
+
this.$emit('transitioned', { action: verb.id, to: String(outcome.record?.state ?? ''), object: outcome.record || null })
|
|
5907
|
+
} finally {
|
|
5908
|
+
this.sourceVerbBusy = false
|
|
5909
|
+
}
|
|
5910
|
+
},
|
|
5911
|
+
|
|
5555
5912
|
/**
|
|
5556
5913
|
* Re-fetch the page's object after a lifecycle transition so the new state
|
|
5557
5914
|
* (and any other server-side mutations the guard applied) render.
|
|
@@ -6328,15 +6685,24 @@ export default {
|
|
|
6328
6685
|
|
|
6329
6686
|
/**
|
|
6330
6687
|
* Whether a side entry belongs beside the open body tab: an entry with
|
|
6331
|
-
* `tabs` (tab ids) shows only while one of them is open; an entry
|
|
6332
|
-
*
|
|
6688
|
+
* `tabs` (tab ids) shows only while one of them is open; an entry with
|
|
6689
|
+
* `notTabs` stands aside while one of those is open (a tab that draws
|
|
6690
|
+
* the side column as its own, DqZaakAssistent); an entry with neither
|
|
6691
|
+
* shows beside every tab. Edit mode shows them all.
|
|
6333
6692
|
*
|
|
6334
6693
|
* @spec openspec/changes/detail-side-cards-per-tab/specs/detail-page/spec.md#requirement-a-side-card-can-follow-the-open-tab
|
|
6694
|
+
* @spec openspec/changes/side-card-not-on-tabs/specs/detail-page/spec.md#requirement-a-side-card-can-stand-aside-for-a-tab
|
|
6335
6695
|
* @param {object} widget The side entry's definition.
|
|
6336
6696
|
* @return {boolean}
|
|
6337
6697
|
*/
|
|
6338
6698
|
sideWidgetOnTab(widget) {
|
|
6339
|
-
if (this.editingBody
|
|
6699
|
+
if (this.editingBody) {
|
|
6700
|
+
return true
|
|
6701
|
+
}
|
|
6702
|
+
if (Array.isArray(widget.notTabs) && widget.notTabs.map((id) => String(id)).includes(this.activeBodyTab)) {
|
|
6703
|
+
return false
|
|
6704
|
+
}
|
|
6705
|
+
if (!Array.isArray(widget.tabs) || widget.tabs.length === 0) {
|
|
6340
6706
|
return true
|
|
6341
6707
|
}
|
|
6342
6708
|
return widget.tabs.map((id) => String(id)).includes(this.activeBodyTab)
|
|
@@ -6772,7 +7138,8 @@ export default {
|
|
|
6772
7138
|
return
|
|
6773
7139
|
}
|
|
6774
7140
|
const r = this.resolvedSidebar
|
|
6775
|
-
|
|
7141
|
+
// A record held back for its access purpose shows nothing in the sidebar either.
|
|
7142
|
+
const hasRecord = !this.objectNotFound && !this.awaitingObject && !this.contentWithheld
|
|
6776
7143
|
if (this.sidebarActive && hasRecord && this.resolvedObjectType && this.objectId) {
|
|
6777
7144
|
const merged = this.mergeSidebarSources(r)
|
|
6778
7145
|
// Seed `open` only on the inactive→active edge (first activation
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
<CnWidgetWrapper
|
|
68
68
|
v-if="missingApp"
|
|
69
69
|
:title="widgetTitle"
|
|
70
|
+
:subtitle="widgetSubtitle"
|
|
70
71
|
:chromeless="isBare"
|
|
71
72
|
titleIconPosition="left"
|
|
72
73
|
:showRefresh="false"
|
|
@@ -207,7 +208,7 @@
|
|
|
207
208
|
<CnWidgetWrapper
|
|
208
209
|
v-else-if="renderer && isContentOnly && !isBare"
|
|
209
210
|
:title="widget.title || ''"
|
|
210
|
-
:subtitle="rendererHeader.subtitle"
|
|
211
|
+
:subtitle="rendererHeader.subtitle || widgetSubtitle"
|
|
211
212
|
titleIconPosition="left"
|
|
212
213
|
:showActions="actionsShown || catalogAddEnabled"
|
|
213
214
|
:showRefresh="false"
|
|
@@ -248,6 +249,7 @@
|
|
|
248
249
|
<CnWidgetWrapper
|
|
249
250
|
v-else-if="renderer && isCard && !isBare"
|
|
250
251
|
:title="widget.title || content.title || ''"
|
|
252
|
+
:subtitle="widgetSubtitle"
|
|
251
253
|
:showTitle="effectiveShowCardTitle"
|
|
252
254
|
titleIconPosition="left"
|
|
253
255
|
flush
|
|
@@ -270,7 +272,7 @@
|
|
|
270
272
|
<CnWidgetWrapper
|
|
271
273
|
v-else-if="renderer && wrapsConsumerWidget"
|
|
272
274
|
:title="widget.title || ''"
|
|
273
|
-
:subtitle="rendererHeader.subtitle"
|
|
275
|
+
:subtitle="rendererHeader.subtitle || widgetSubtitle"
|
|
274
276
|
:showTitle="effectiveShowCardTitle"
|
|
275
277
|
titleIconPosition="left"
|
|
276
278
|
:showActions="false"
|
|
@@ -754,6 +756,17 @@ export default {
|
|
|
754
756
|
return widgetTitleOf(this.widget) || ''
|
|
755
757
|
},
|
|
756
758
|
|
|
759
|
+
/**
|
|
760
|
+
* The muted line under a card's title (`subtitle` on the widget or in
|
|
761
|
+
* its content), translated by the card. Empty when neither sets one.
|
|
762
|
+
*
|
|
763
|
+
* @return {string} The subtitle.
|
|
764
|
+
*/
|
|
765
|
+
widgetSubtitle() {
|
|
766
|
+
const subtitle = (this.widget && this.widget.subtitle) || this.content.subtitle || ''
|
|
767
|
+
return typeof subtitle === 'string' ? subtitle : ''
|
|
768
|
+
},
|
|
769
|
+
|
|
757
770
|
/**
|
|
758
771
|
* Headline for the set-up state.
|
|
759
772
|
*
|