@conduction/nextcloud-vue 2.119.0 → 2.121.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/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js +9 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js +226 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js +211 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js +9 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js +64 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js +89 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js.map +1 -0
- package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
- package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +53 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +546 -523
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js +9 -0
- package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js +9 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js.map +1 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js +257 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js.map +1 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js +266 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js +14 -7
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js +9 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js +278 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js +128 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js +9 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js +214 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js +125 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js +9 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js +163 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js +131 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js +127 -0
- package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js +9 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js +82 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js +61 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js +60 -0
- package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js +10 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js +226 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js +126 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js +10 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js +944 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js +276 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js +9 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js +354 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js +392 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js +9 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js +180 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js +208 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js +9 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js +244 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js +76 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js +290 -0
- package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +9 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +448 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +303 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +19 -2
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +73 -62
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +6 -0
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +42 -32
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +116 -16
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +207 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +207 -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 +951 -16
- package/dist/esm/utils/aiFeatures.js +245 -0
- package/dist/esm/utils/aiFeatures.js.map +1 -0
- package/dist/esm/utils/aiSuggestionTracker.js +182 -0
- package/dist/esm/utils/aiSuggestionTracker.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +2 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/objectPermissions.js +552 -0
- package/dist/esm/utils/objectPermissions.js.map +1 -0
- package/dist/esm/utils/objectTranslations.js +180 -0
- package/dist/esm/utils/objectTranslations.js.map +1 -0
- package/dist/esm/utils/stateMachineValidation.js +254 -0
- package/dist/esm/utils/stateMachineValidation.js.map +1 -0
- package/dist/esm/utils/widgetDispatch.js +3 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +23038 -14567
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +951 -16
- package/l10n/en.json +207 -1
- package/l10n/nl.json +207 -1
- package/package.json +1 -1
- package/src/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue +416 -0
- package/src/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue +136 -0
- package/src/components/CnAiFeaturesWidget/__tests__/CnAiFeaturesWidget.spec.js +213 -0
- package/src/components/CnAiFeaturesWidget/index.js +14 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +74 -3
- package/src/components/CnIndexPage/useNamedSource.js +9 -0
- package/src/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue +499 -0
- package/src/components/CnObjectPermissionsTab/__tests__/CnObjectPermissionsTab.spec.js +186 -0
- package/src/components/CnObjectPermissionsTab/index.js +13 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +8 -1
- package/src/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue +424 -0
- package/src/components/CnObjectTranslationsWidget/__tests__/CnObjectTranslationsWidget.spec.js +189 -0
- package/src/components/CnObjectTranslationsWidget/index.js +13 -0
- package/src/components/CnReadinessChecklist/CnReadinessChecklist.vue +335 -0
- package/src/components/CnReadinessChecklist/index.js +9 -0
- package/src/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue +300 -0
- package/src/components/CnSchemaRulesWidget/index.js +10 -0
- package/src/components/CnSchemaRulesWidget/schemaRules.js +123 -0
- package/src/components/CnSearchIndexStatus/CnSearchIndexStatus.vue +160 -0
- package/src/components/CnSearchIndexStatus/index.js +10 -0
- package/src/components/CnSearchIndexStatus/searchIndexStatus.js +56 -0
- package/src/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue +328 -0
- package/src/components/CnShippedConfigurationSection/index.js +9 -0
- package/src/components/CnStateMachineEditor/CnStateMachineEditor.vue +1332 -0
- package/src/components/CnStateMachineEditor/CnStateMachineFields.vue +791 -0
- package/src/components/CnStateMachineEditor/CnStateMachineNode.vue +442 -0
- package/src/components/CnStateMachineEditor/CnStateMachinePanel.vue +326 -0
- package/src/components/CnStateMachineEditor/index.js +8 -0
- package/src/components/CnStateMachineEditor/stateMachineFields.js +287 -0
- package/src/components/CnTimelineWidget/CnTimelineManage.vue +827 -0
- package/src/components/CnTimelineWidget/CnTimelineWidget.vue +29 -3
- package/src/components/CnWidgetGrid/builtInWidgets.js +6 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +10 -0
- package/src/components/index.js +8 -0
- package/src/composables/indexSources.js +116 -16
- package/src/css/index-page.css +7 -0
- package/src/index.js +9 -0
- package/src/utils/aiFeatures.js +241 -0
- package/src/utils/aiSuggestionTracker.js +178 -0
- package/src/utils/libraryWidgetKeys.js +2 -0
- package/src/utils/objectPermissions.js +548 -0
- package/src/utils/objectTranslations.js +176 -0
- package/src/utils/stateMachineValidation.js +250 -0
- package/src/utils/widgetDispatch.js +3 -1
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
|
|
5
|
+
CnTimelineManage — the `manage` mode of CnTimelineWidget: one chronological
|
|
6
|
+
read of OpenRegister's timeline on an object (notes, calls, messages), newest
|
|
7
|
+
first with pinned entries on top, plus what a reader who may manage it can do
|
|
8
|
+
there: filter by kind and visibility, pin, close a follow-up, write a note
|
|
9
|
+
(optionally from a standard text block, optionally visible outside), export
|
|
10
|
+
the shown entries as CSV, and run configured entry actions such as a reply
|
|
11
|
+
dialog.
|
|
12
|
+
|
|
13
|
+
The list read is filtered by OpenRegister whether or not a filter is asked
|
|
14
|
+
for; the response says whether the reader may manage (`canManage`), and the
|
|
15
|
+
controls are drawn from that rather than guessed. A failed read is not an
|
|
16
|
+
empty history.
|
|
17
|
+
-->
|
|
18
|
+
<template>
|
|
19
|
+
<div class="cn-timeline-manage" data-testid="cn-timeline-manage">
|
|
20
|
+
<div class="cn-timeline-manage__header">
|
|
21
|
+
<div class="cn-timeline-manage__heading">
|
|
22
|
+
<h3 class="cn-timeline-manage__title">
|
|
23
|
+
{{ titleText }}
|
|
24
|
+
</h3>
|
|
25
|
+
<p class="cn-timeline-manage__subtitle">
|
|
26
|
+
{{ subtitleText }}
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="cn-timeline-manage__header-actions">
|
|
30
|
+
<NcButton v-if="canManage && composerEnabled"
|
|
31
|
+
data-testid="cn-timeline-add-note"
|
|
32
|
+
@click="focusComposer">
|
|
33
|
+
<template #icon>
|
|
34
|
+
<Plus :size="20" />
|
|
35
|
+
</template>
|
|
36
|
+
{{ t('nextcloud-vue', 'Add a note') }}
|
|
37
|
+
</NcButton>
|
|
38
|
+
<NcButton data-testid="cn-timeline-export" @click="exportEntries">
|
|
39
|
+
<template #icon>
|
|
40
|
+
<Download :size="20" />
|
|
41
|
+
</template>
|
|
42
|
+
{{ t('nextcloud-vue', 'Export') }}
|
|
43
|
+
</NcButton>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="cn-timeline-manage__chips"
|
|
48
|
+
role="group"
|
|
49
|
+
:aria-label="t('nextcloud-vue', 'Kind')"
|
|
50
|
+
data-testid="cn-timeline-kind-filter">
|
|
51
|
+
<button v-for="chip in kindChips"
|
|
52
|
+
:key="chip.key"
|
|
53
|
+
type="button"
|
|
54
|
+
class="cn-timeline-manage__chip"
|
|
55
|
+
:class="{ 'cn-timeline-manage__chip--active': chip.active }"
|
|
56
|
+
:aria-pressed="chip.active ? 'true' : 'false'"
|
|
57
|
+
:data-testid="'cn-timeline-chip-' + chip.key"
|
|
58
|
+
@click="chooseKind(chip.id)">
|
|
59
|
+
{{ chip.label }}
|
|
60
|
+
<span class="cn-timeline-manage__chip-count">{{ chip.count }}</span>
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="cn-timeline-manage__controls">
|
|
65
|
+
<NcSelect v-if="canManage"
|
|
66
|
+
v-model="visibilityFilter"
|
|
67
|
+
class="cn-timeline-manage__filter"
|
|
68
|
+
:options="visibilityOptions"
|
|
69
|
+
:clearable="true"
|
|
70
|
+
label="label"
|
|
71
|
+
:inputLabel="t('nextcloud-vue', 'Visibility')"
|
|
72
|
+
:placeholder="t('nextcloud-vue', 'Internal and public')"
|
|
73
|
+
data-testid="cn-timeline-visibility-filter"
|
|
74
|
+
@update:modelValue="load" />
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<NcLoadingIcon v-if="loading" :size="32" />
|
|
78
|
+
|
|
79
|
+
<NcNoteCard v-else-if="error" type="error" data-testid="cn-timeline-error">
|
|
80
|
+
{{ error }}
|
|
81
|
+
</NcNoteCard>
|
|
82
|
+
|
|
83
|
+
<NcEmptyContent v-else-if="entries.length === 0"
|
|
84
|
+
:name="t('nextcloud-vue', 'Nothing recorded yet')"
|
|
85
|
+
:description="t('nextcloud-vue', 'Notes, calls and messages appear here in the order they happened.')"
|
|
86
|
+
data-testid="cn-timeline-empty" />
|
|
87
|
+
|
|
88
|
+
<ul v-else class="cn-timeline-manage__list" data-testid="cn-timeline-list">
|
|
89
|
+
<li v-for="entry in entries"
|
|
90
|
+
:key="entry.id"
|
|
91
|
+
class="cn-timeline-manage__entry"
|
|
92
|
+
:class="{ 'cn-timeline-manage__entry--pinned': entry.pinned }"
|
|
93
|
+
:data-kind="entry.kind || 'note'"
|
|
94
|
+
data-testid="cn-timeline-entry">
|
|
95
|
+
<div class="cn-timeline-manage__head">
|
|
96
|
+
<span class="cn-timeline-manage__kind">{{ kindLabel(entry) }}</span>
|
|
97
|
+
<span class="cn-timeline-manage__author">{{ entry.author }}</span>
|
|
98
|
+
<span class="cn-timeline-manage__moment">{{ moment(entry.created) }}</span>
|
|
99
|
+
<span v-if="entry.visibility === 'public'"
|
|
100
|
+
class="cn-timeline-manage__badge"
|
|
101
|
+
data-testid="cn-timeline-public">
|
|
102
|
+
{{ publicLabel }}
|
|
103
|
+
</span>
|
|
104
|
+
<span v-if="entry.followUp === 'open'"
|
|
105
|
+
class="cn-timeline-manage__badge cn-timeline-manage__badge--open"
|
|
106
|
+
data-testid="cn-timeline-followup-open">
|
|
107
|
+
{{ t('nextcloud-vue', 'Needs an answer') }}
|
|
108
|
+
</span>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<p class="cn-timeline-manage__message">
|
|
112
|
+
{{ entry.message }}
|
|
113
|
+
</p>
|
|
114
|
+
|
|
115
|
+
<p v-if="entry.siblings && entry.siblings.length"
|
|
116
|
+
class="cn-timeline-manage__siblings"
|
|
117
|
+
data-testid="cn-timeline-siblings">
|
|
118
|
+
{{ n('nextcloud-vue', 'Also written on %n other record', 'Also written on %n other records', entry.siblings.length) }}
|
|
119
|
+
</p>
|
|
120
|
+
|
|
121
|
+
<div v-if="canManage" class="cn-timeline-manage__actions">
|
|
122
|
+
<NcButton variant="tertiary"
|
|
123
|
+
:data-testid="'cn-timeline-pin-' + entry.id"
|
|
124
|
+
@click="togglePin(entry)">
|
|
125
|
+
{{ entry.pinned ? t('nextcloud-vue', 'Unpin') : t('nextcloud-vue', 'Pin') }}
|
|
126
|
+
</NcButton>
|
|
127
|
+
<NcButton v-for="action in actionsFor(entry)"
|
|
128
|
+
:key="action.id"
|
|
129
|
+
variant="tertiary"
|
|
130
|
+
:data-testid="'cn-timeline-action-' + action.id + '-' + entry.id"
|
|
131
|
+
@click="openAction(action, entry)">
|
|
132
|
+
{{ translate(action.label) }}
|
|
133
|
+
</NcButton>
|
|
134
|
+
<NcButton v-if="entry.followUp === 'open'"
|
|
135
|
+
variant="tertiary"
|
|
136
|
+
:data-testid="'cn-timeline-followup-' + entry.id"
|
|
137
|
+
@click="closeFollowUp(entry)">
|
|
138
|
+
{{ t('nextcloud-vue', 'Mark as answered') }}
|
|
139
|
+
</NcButton>
|
|
140
|
+
</div>
|
|
141
|
+
</li>
|
|
142
|
+
</ul>
|
|
143
|
+
|
|
144
|
+
<p v-if="!loading && !error && allEntries.length > 0"
|
|
145
|
+
class="cn-timeline-manage__count"
|
|
146
|
+
data-testid="cn-timeline-count">
|
|
147
|
+
{{ t('nextcloud-vue', '{shown} of {total} events', { shown: entries.length, total: allEntries.length }) }}
|
|
148
|
+
</p>
|
|
149
|
+
|
|
150
|
+
<form v-if="canManage && composerEnabled"
|
|
151
|
+
ref="composer"
|
|
152
|
+
class="cn-timeline-manage__composer"
|
|
153
|
+
data-testid="cn-timeline-composer"
|
|
154
|
+
@submit.prevent="submit">
|
|
155
|
+
<h3 class="cn-timeline-manage__composer-title">
|
|
156
|
+
{{ t('nextcloud-vue', 'Add a note') }}
|
|
157
|
+
</h3>
|
|
158
|
+
<p class="cn-timeline-manage__composer-hint">
|
|
159
|
+
{{ t('nextcloud-vue', 'Goes on the timeline above, internal unless you make it visible') }}
|
|
160
|
+
</p>
|
|
161
|
+
<NcSelect v-if="textBlocks.length"
|
|
162
|
+
v-model="chosenBlock"
|
|
163
|
+
class="cn-timeline-manage__filter"
|
|
164
|
+
:options="blockOptions"
|
|
165
|
+
:clearable="true"
|
|
166
|
+
label="label"
|
|
167
|
+
:inputLabel="t('nextcloud-vue', 'Standard note')"
|
|
168
|
+
:placeholder="t('nextcloud-vue', 'Write your own')"
|
|
169
|
+
data-testid="cn-timeline-block"
|
|
170
|
+
@update:modelValue="pickBlock" />
|
|
171
|
+
|
|
172
|
+
<label class="cn-timeline-manage__draft-label" :for="draftId">
|
|
173
|
+
{{ t('nextcloud-vue', 'Write a note on this record') }}
|
|
174
|
+
</label>
|
|
175
|
+
<textarea :id="draftId"
|
|
176
|
+
v-model="draft"
|
|
177
|
+
class="cn-timeline-manage__draft"
|
|
178
|
+
:placeholder="t('nextcloud-vue', 'Write a note on this record')"
|
|
179
|
+
data-testid="cn-timeline-draft" />
|
|
180
|
+
|
|
181
|
+
<NcCheckboxRadioSwitch v-model="draftIsPublic" data-testid="cn-timeline-draft-public">
|
|
182
|
+
{{ publicLabel }}
|
|
183
|
+
</NcCheckboxRadioSwitch>
|
|
184
|
+
|
|
185
|
+
<NcButton variant="primary"
|
|
186
|
+
type="submit"
|
|
187
|
+
:disabled="saving || !draft.trim()"
|
|
188
|
+
data-testid="cn-timeline-save">
|
|
189
|
+
{{ t('nextcloud-vue', 'Add to the timeline') }}
|
|
190
|
+
</NcButton>
|
|
191
|
+
</form>
|
|
192
|
+
|
|
193
|
+
<component :is="activeModal.component"
|
|
194
|
+
v-if="activeModal"
|
|
195
|
+
v-bind="activeModal.props"
|
|
196
|
+
v-on="activeModal.listeners" />
|
|
197
|
+
</div>
|
|
198
|
+
</template>
|
|
199
|
+
|
|
200
|
+
<script>
|
|
201
|
+
import axios from '@nextcloud/axios'
|
|
202
|
+
import { translatePlural as n, translate as t } from '@nextcloud/l10n'
|
|
203
|
+
import { NcButton, NcCheckboxRadioSwitch, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcSelect } from '@nextcloud/vue'
|
|
204
|
+
import Download from 'vue-material-design-icons/Download.vue'
|
|
205
|
+
import Plus from 'vue-material-design-icons/Plus.vue'
|
|
206
|
+
import { prefixUrl } from '../../utils/headers.js'
|
|
207
|
+
|
|
208
|
+
let instanceCount = 0
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Read a dotted path off a value.
|
|
212
|
+
*
|
|
213
|
+
* @param {object} value The value.
|
|
214
|
+
* @param {string} path The path.
|
|
215
|
+
* @return {unknown} What is there, or undefined.
|
|
216
|
+
*/
|
|
217
|
+
function readPath(value, path) {
|
|
218
|
+
return String(path).split('.').reduce((at, key) => (at === null || at === undefined ? undefined : at[key]), value)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export default {
|
|
222
|
+
name: 'CnTimelineManage',
|
|
223
|
+
|
|
224
|
+
components: { Download, NcButton, NcCheckboxRadioSwitch, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcSelect, Plus },
|
|
225
|
+
|
|
226
|
+
inject: {
|
|
227
|
+
/** The app's component registry; entry actions resolve their modal here. */
|
|
228
|
+
cnRegistry: { default: () => ({}) },
|
|
229
|
+
/** Consumer translation function; manifest labels are authored in English. */
|
|
230
|
+
cnTranslate: { default: () => (key) => key },
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
props: {
|
|
234
|
+
/** Register slug. */
|
|
235
|
+
register: { type: String, default: '' },
|
|
236
|
+
/** Schema slug. */
|
|
237
|
+
schema: { type: String, default: '' },
|
|
238
|
+
/** The object's id. */
|
|
239
|
+
objectId: { type: String, default: '' },
|
|
240
|
+
/** OpenRegister API base. */
|
|
241
|
+
apiBase: { type: String, default: '/apps/openregister/api' },
|
|
242
|
+
/**
|
|
243
|
+
* The widget content: `{ title?, subtitle?, publicLabel?, exportName?,
|
|
244
|
+
* composer?, textBlocks?, entryActions? }`.
|
|
245
|
+
*/
|
|
246
|
+
content: { type: Object, default: () => ({}) },
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
data() {
|
|
250
|
+
instanceCount += 1
|
|
251
|
+
return {
|
|
252
|
+
uid: instanceCount,
|
|
253
|
+
allEntries: [],
|
|
254
|
+
entries: [],
|
|
255
|
+
kinds: [],
|
|
256
|
+
loading: false,
|
|
257
|
+
error: '',
|
|
258
|
+
canManage: false,
|
|
259
|
+
kindFilter: null,
|
|
260
|
+
visibilityFilter: null,
|
|
261
|
+
textBlocks: [],
|
|
262
|
+
chosenBlock: null,
|
|
263
|
+
draft: '',
|
|
264
|
+
draftIsPublic: false,
|
|
265
|
+
saving: false,
|
|
266
|
+
activeModal: null,
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
computed: {
|
|
271
|
+
titleText() {
|
|
272
|
+
return this.content.title ? this.translate(this.content.title) : t('nextcloud-vue', 'History')
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
subtitleText() {
|
|
276
|
+
return this.content.subtitle ? this.translate(this.content.subtitle) : t('nextcloud-vue', 'What happened, who did it and when, newest first')
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
publicLabel() {
|
|
280
|
+
return this.content.publicLabel ? this.translate(this.content.publicLabel) : t('nextcloud-vue', 'Visible to the applicant')
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
composerEnabled() {
|
|
284
|
+
return this.content.composer !== false
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
draftId() {
|
|
288
|
+
return `cn-timeline-draft-${this.uid}`
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The kind options, the plain note first. A note is an entry with NO
|
|
293
|
+
* kind, so "Note" filters on its absence.
|
|
294
|
+
*
|
|
295
|
+
* @return {Array<object>} The options.
|
|
296
|
+
*/
|
|
297
|
+
kindOptions() {
|
|
298
|
+
return [{ id: '', label: t('nextcloud-vue', 'Note') }].concat(this.kinds.map((kind) => ({ id: kind.slug, label: kind.title || kind.slug })))
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* All, then every kind the object has entries of, each with its count.
|
|
303
|
+
*
|
|
304
|
+
* @return {Array<object>} The chips.
|
|
305
|
+
*/
|
|
306
|
+
kindChips() {
|
|
307
|
+
const chosen = this.kindFilter ? (this.kindFilter.id || '') : null
|
|
308
|
+
const counts = new Map()
|
|
309
|
+
for (const entry of this.allEntries) {
|
|
310
|
+
const kind = entry.kind || ''
|
|
311
|
+
counts.set(kind, (counts.get(kind) || 0) + 1)
|
|
312
|
+
}
|
|
313
|
+
const chips = [{ key: 'all', id: null, label: t('nextcloud-vue', 'All'), count: this.allEntries.length, active: chosen === null }]
|
|
314
|
+
for (const option of this.kindOptions) {
|
|
315
|
+
const count = counts.get(option.id) || 0
|
|
316
|
+
if (count > 0) {
|
|
317
|
+
chips.push({ key: option.id || 'note', id: option.id, label: option.label, count, active: chosen === option.id })
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return chips
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
blockOptions() {
|
|
324
|
+
return this.textBlocks.map((block) => ({ id: block.slug, label: block.title || block.slug }))
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
visibilityOptions() {
|
|
328
|
+
return [
|
|
329
|
+
{ id: 'internal', label: t('nextcloud-vue', 'Internal only') },
|
|
330
|
+
{ id: 'public', label: this.publicLabel },
|
|
331
|
+
]
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
base() {
|
|
335
|
+
return this.apiBase.replace(/\/$/, '')
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
timelineUrl() {
|
|
339
|
+
if (!this.objectId || !this.register || !this.schema) {
|
|
340
|
+
return ''
|
|
341
|
+
}
|
|
342
|
+
return prefixUrl(`${this.base}/objects/${this.register}/${this.schema}/${this.objectId}/timeline`)
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
entryActions() {
|
|
346
|
+
return Array.isArray(this.content.entryActions) ? this.content.entryActions.filter((action) => action && action.id) : []
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
watch: {
|
|
351
|
+
objectId: 'load',
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
mounted() {
|
|
355
|
+
this.loadKinds()
|
|
356
|
+
if (this.content.textBlocks !== false) {
|
|
357
|
+
this.loadTextBlocks()
|
|
358
|
+
}
|
|
359
|
+
this.load()
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
methods: {
|
|
363
|
+
t,
|
|
364
|
+
n,
|
|
365
|
+
|
|
366
|
+
translate(text) {
|
|
367
|
+
return text ? this.cnTranslate(text) : ''
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
/** Read the declared kinds, so the filter names them as administered. */
|
|
371
|
+
async loadKinds() {
|
|
372
|
+
try {
|
|
373
|
+
const response = await axios.get(prefixUrl(`${this.base}/timeline/kinds`))
|
|
374
|
+
this.kinds = response.data?.results || []
|
|
375
|
+
} catch {
|
|
376
|
+
this.kinds = []
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
/** Read the seeded standard notes. */
|
|
381
|
+
async loadTextBlocks() {
|
|
382
|
+
try {
|
|
383
|
+
const response = await axios.get(prefixUrl(`${this.base}/timeline/text-blocks`))
|
|
384
|
+
this.textBlocks = response.data?.results || []
|
|
385
|
+
} catch {
|
|
386
|
+
this.textBlocks = []
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Note which standard note was picked. The BLOCK is sent, not its body:
|
|
392
|
+
* OpenRegister fills the object's own values into it on the way in.
|
|
393
|
+
*
|
|
394
|
+
* @param {object|null} block The picked option.
|
|
395
|
+
*/
|
|
396
|
+
pickBlock(block) {
|
|
397
|
+
this.chosenBlock = block || null
|
|
398
|
+
if (block) {
|
|
399
|
+
const declared = this.textBlocks.find((entry) => entry.slug === block.id)
|
|
400
|
+
this.draft = declared?.title || block.label
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Write the drafted note onto this object.
|
|
406
|
+
*
|
|
407
|
+
* @return {Promise<void>}
|
|
408
|
+
*/
|
|
409
|
+
async submit() {
|
|
410
|
+
if (!this.draft.trim() || !this.timelineUrl) {
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
this.saving = true
|
|
414
|
+
this.error = ''
|
|
415
|
+
try {
|
|
416
|
+
const payload = { visibility: this.draftIsPublic ? 'public' : 'internal' }
|
|
417
|
+
if (this.chosenBlock?.id) {
|
|
418
|
+
payload.textBlock = this.chosenBlock.id
|
|
419
|
+
} else {
|
|
420
|
+
payload.message = this.draft
|
|
421
|
+
}
|
|
422
|
+
await axios.post(this.timelineUrl, payload)
|
|
423
|
+
this.draft = ''
|
|
424
|
+
this.chosenBlock = null
|
|
425
|
+
this.draftIsPublic = false
|
|
426
|
+
await this.load()
|
|
427
|
+
} catch {
|
|
428
|
+
this.error = t('nextcloud-vue', 'That note could not be saved.')
|
|
429
|
+
} finally {
|
|
430
|
+
this.saving = false
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Read the timeline with the visibility filter, if one is set.
|
|
436
|
+
*
|
|
437
|
+
* @return {Promise<void>}
|
|
438
|
+
*/
|
|
439
|
+
async load() {
|
|
440
|
+
if (!this.timelineUrl) {
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
this.loading = true
|
|
444
|
+
this.error = ''
|
|
445
|
+
try {
|
|
446
|
+
const params = {}
|
|
447
|
+
if (this.visibilityFilter?.id) {
|
|
448
|
+
params.visibility = this.visibilityFilter.id
|
|
449
|
+
}
|
|
450
|
+
const response = await axios.get(this.timelineUrl, { params })
|
|
451
|
+
this.canManage = response.data?.canManage === true
|
|
452
|
+
this.allEntries = response.data?.results || []
|
|
453
|
+
this.entries = this.applyKindFilter(this.allEntries)
|
|
454
|
+
} catch {
|
|
455
|
+
this.allEntries = []
|
|
456
|
+
this.entries = []
|
|
457
|
+
this.error = t('nextcloud-vue', 'The timeline could not be read. Try again, or ask an administrator to check OpenRegister.')
|
|
458
|
+
} finally {
|
|
459
|
+
this.loading = false
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Show one kind, or every kind for `null`, without reading again.
|
|
465
|
+
*
|
|
466
|
+
* @param {string|null} id The kind slug, '' for a note, null for all.
|
|
467
|
+
*/
|
|
468
|
+
chooseKind(id) {
|
|
469
|
+
this.kindFilter = id === null ? null : { id }
|
|
470
|
+
this.entries = this.applyKindFilter(this.allEntries)
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* The kind filter is applied in the reading: the list endpoint does not
|
|
475
|
+
* read a kind parameter, so sending one would be dropped in silence.
|
|
476
|
+
*
|
|
477
|
+
* @param {Array<object>} results The entries as read.
|
|
478
|
+
* @return {Array<object>} The entries to show.
|
|
479
|
+
*/
|
|
480
|
+
applyKindFilter(results) {
|
|
481
|
+
if (!this.kindFilter) {
|
|
482
|
+
return results
|
|
483
|
+
}
|
|
484
|
+
const wanted = this.kindFilter.id || ''
|
|
485
|
+
return results.filter((entry) => (entry.kind || '') === wanted)
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* The shown entries as CSV: when, kind, who, visibility and what.
|
|
490
|
+
*
|
|
491
|
+
* @return {string} The CSV text.
|
|
492
|
+
*/
|
|
493
|
+
entriesCsv() {
|
|
494
|
+
const cell = (value) => `"${String(value ?? '').replace(/"/g, '""')}"`
|
|
495
|
+
const rows = [[t('nextcloud-vue', 'When'), t('nextcloud-vue', 'Kind'), t('nextcloud-vue', 'Who'), t('nextcloud-vue', 'Visibility'), t('nextcloud-vue', 'Message')]]
|
|
496
|
+
for (const entry of this.entries) {
|
|
497
|
+
rows.push([entry.created, this.kindLabel(entry), entry.author, entry.visibility, entry.message])
|
|
498
|
+
}
|
|
499
|
+
return rows.map((row) => row.map(cell).join(',')).join('\r\n')
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
/** Download the shown entries as a CSV file. */
|
|
503
|
+
exportEntries() {
|
|
504
|
+
const blob = new Blob([this.entriesCsv()], { type: 'text/csv;charset=utf-8' })
|
|
505
|
+
const url = URL.createObjectURL(blob)
|
|
506
|
+
const link = document.createElement('a')
|
|
507
|
+
link.href = url
|
|
508
|
+
link.download = `${this.content.exportName || 'history'}-${this.objectId || 'record'}.csv`
|
|
509
|
+
document.body.appendChild(link)
|
|
510
|
+
link.click()
|
|
511
|
+
link.remove()
|
|
512
|
+
URL.revokeObjectURL(url)
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
/** Bring the composer into view and focus its text. */
|
|
516
|
+
focusComposer() {
|
|
517
|
+
const form = this.$refs.composer
|
|
518
|
+
if (!form) {
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
form.scrollIntoView?.({ behavior: 'smooth', block: 'center' })
|
|
522
|
+
form.querySelector('textarea')?.focus()
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* The label of one entry's kind, falling back to "Note".
|
|
527
|
+
*
|
|
528
|
+
* @param {object} entry The entry.
|
|
529
|
+
* @return {string} The label.
|
|
530
|
+
*/
|
|
531
|
+
kindLabel(entry) {
|
|
532
|
+
if (!entry.kind) {
|
|
533
|
+
return t('nextcloud-vue', 'Note')
|
|
534
|
+
}
|
|
535
|
+
const declared = this.kinds.find((kind) => kind.slug === entry.kind)
|
|
536
|
+
return declared?.title || entry.kind
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* One moment in the reader's locale.
|
|
541
|
+
*
|
|
542
|
+
* @param {string} value An ISO 8601 stamp.
|
|
543
|
+
* @return {string} The moment, or the raw value.
|
|
544
|
+
*/
|
|
545
|
+
moment(value) {
|
|
546
|
+
if (!value) {
|
|
547
|
+
return ''
|
|
548
|
+
}
|
|
549
|
+
const parsed = new Date(value)
|
|
550
|
+
return isNaN(parsed.getTime()) ? value : parsed.toLocaleString()
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
async togglePin(entry) {
|
|
554
|
+
await this.patch(entry, { pinned: !entry.pinned })
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
async closeFollowUp(entry) {
|
|
558
|
+
await this.patch(entry, { followUp: 'done' })
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Write one change onto one entry and read the timeline back (pinning
|
|
563
|
+
* changes the order, so patching in place would look like nothing).
|
|
564
|
+
*
|
|
565
|
+
* @param {object} entry The entry.
|
|
566
|
+
* @param {object} changes The fields to write.
|
|
567
|
+
* @return {Promise<void>}
|
|
568
|
+
*/
|
|
569
|
+
async patch(entry, changes) {
|
|
570
|
+
try {
|
|
571
|
+
await axios.patch(`${this.timelineUrl}/${entry.id}`, changes)
|
|
572
|
+
await this.load()
|
|
573
|
+
} catch {
|
|
574
|
+
this.error = t('nextcloud-vue', 'That change could not be saved.')
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* The configured entry actions that apply to one entry (`kinds` limits
|
|
580
|
+
* an action to entries of those kinds).
|
|
581
|
+
*
|
|
582
|
+
* @param {object} entry The entry.
|
|
583
|
+
* @return {Array<object>} The actions.
|
|
584
|
+
*/
|
|
585
|
+
actionsFor(entry) {
|
|
586
|
+
return this.entryActions.filter((action) => !Array.isArray(action.kinds) || action.kinds.includes(entry.kind || ''))
|
|
587
|
+
},
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Resolve `@objectId` and `@row.<path>` tokens in an action's props.
|
|
591
|
+
*
|
|
592
|
+
* @param {object} props The props as declared.
|
|
593
|
+
* @param {object} entry The entry.
|
|
594
|
+
* @return {object} The resolved props.
|
|
595
|
+
*/
|
|
596
|
+
resolveProps(props, entry) {
|
|
597
|
+
const out = {}
|
|
598
|
+
for (const [key, value] of Object.entries(props || {})) {
|
|
599
|
+
if (value === '@objectId') {
|
|
600
|
+
out[key] = this.objectId
|
|
601
|
+
} else if (typeof value === 'string' && value.startsWith('@row.')) {
|
|
602
|
+
const found = readPath(entry, value.slice('@row.'.length))
|
|
603
|
+
out[key] = found === undefined || found === null ? '' : found
|
|
604
|
+
} else {
|
|
605
|
+
out[key] = value
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return out
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Open an entry action's modal from the app's registry. On its
|
|
613
|
+
* `doneEvent` (default `done`) the entry's open follow-up is closed when
|
|
614
|
+
* the action says so; otherwise the timeline is read again.
|
|
615
|
+
*
|
|
616
|
+
* @param {object} action The action.
|
|
617
|
+
* @param {object} entry The entry.
|
|
618
|
+
*/
|
|
619
|
+
openAction(action, entry) {
|
|
620
|
+
const registered = (this.cnRegistry || {})[action.modal]
|
|
621
|
+
const component = registered && (registered.component || registered)
|
|
622
|
+
if (!component) {
|
|
623
|
+
// eslint-disable-next-line no-console
|
|
624
|
+
console.warn(`[CnTimelineWidget] entry action "${action.id}" names modal "${action.modal}", which the registry does not hold.`)
|
|
625
|
+
return
|
|
626
|
+
}
|
|
627
|
+
const close = () => {
|
|
628
|
+
this.activeModal = null
|
|
629
|
+
}
|
|
630
|
+
const done = async () => {
|
|
631
|
+
this.activeModal = null
|
|
632
|
+
if (action.closesFollowUp === true && entry.followUp === 'open') {
|
|
633
|
+
await this.closeFollowUp(entry)
|
|
634
|
+
return
|
|
635
|
+
}
|
|
636
|
+
await this.load()
|
|
637
|
+
}
|
|
638
|
+
this.activeModal = {
|
|
639
|
+
component,
|
|
640
|
+
props: this.resolveProps(action.props, entry),
|
|
641
|
+
listeners: { [action.doneEvent || 'done']: done, close },
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
}
|
|
646
|
+
</script>
|
|
647
|
+
|
|
648
|
+
<style scoped>
|
|
649
|
+
.cn-timeline-manage__header {
|
|
650
|
+
display: flex;
|
|
651
|
+
align-items: flex-start;
|
|
652
|
+
justify-content: space-between;
|
|
653
|
+
gap: 12px;
|
|
654
|
+
margin-block-end: 16px;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.cn-timeline-manage__heading {
|
|
658
|
+
display: flex;
|
|
659
|
+
flex-direction: column;
|
|
660
|
+
gap: 4px;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.cn-timeline-manage__header-actions {
|
|
664
|
+
display: flex;
|
|
665
|
+
gap: 8px;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.cn-timeline-manage__title {
|
|
669
|
+
margin: 0;
|
|
670
|
+
font-size: 17px;
|
|
671
|
+
font-weight: 700;
|
|
672
|
+
line-height: 1.2;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.cn-timeline-manage__subtitle {
|
|
676
|
+
margin: 0;
|
|
677
|
+
color: var(--color-text-maxcontrast);
|
|
678
|
+
font-size: 14px;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.cn-timeline-manage__chips {
|
|
682
|
+
display: flex;
|
|
683
|
+
flex-wrap: wrap;
|
|
684
|
+
gap: 8px;
|
|
685
|
+
margin-block-end: 12px;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.cn-timeline-manage__chip {
|
|
689
|
+
display: inline-flex;
|
|
690
|
+
align-items: center;
|
|
691
|
+
gap: 8px;
|
|
692
|
+
box-sizing: border-box;
|
|
693
|
+
height: 36px;
|
|
694
|
+
padding: 0 12px;
|
|
695
|
+
border: 1px solid var(--color-border-dark);
|
|
696
|
+
border-radius: 18px;
|
|
697
|
+
background: var(--color-main-background);
|
|
698
|
+
color: var(--color-main-text);
|
|
699
|
+
font-size: 14px;
|
|
700
|
+
font-weight: 600;
|
|
701
|
+
cursor: pointer;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/* The chosen kind is a filled chip with no outline. */
|
|
705
|
+
.cn-timeline-manage__chip--active {
|
|
706
|
+
border: 0;
|
|
707
|
+
background: var(--color-main-text);
|
|
708
|
+
color: var(--color-main-background);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.cn-timeline-manage__chip-count {
|
|
712
|
+
padding: 1px 7px;
|
|
713
|
+
border-radius: 10px;
|
|
714
|
+
background: var(--color-border);
|
|
715
|
+
color: var(--color-main-text);
|
|
716
|
+
font-size: 12px;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/* On the filled chip the count is a lighter ink bubble with light text. */
|
|
720
|
+
.cn-timeline-manage__chip--active .cn-timeline-manage__chip-count {
|
|
721
|
+
background: color-mix(in srgb, var(--color-main-background) 18%, var(--color-main-text));
|
|
722
|
+
color: var(--color-main-background);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.cn-timeline-manage__count {
|
|
726
|
+
margin: 12px 0 0;
|
|
727
|
+
color: var(--color-text-maxcontrast);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.cn-timeline-manage__composer {
|
|
731
|
+
display: flex;
|
|
732
|
+
flex-direction: column;
|
|
733
|
+
gap: 8px;
|
|
734
|
+
margin-block-start: 24px;
|
|
735
|
+
padding-block-start: 16px;
|
|
736
|
+
border-block-start: 1px solid var(--color-border);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.cn-timeline-manage__composer-title {
|
|
740
|
+
margin: 0;
|
|
741
|
+
font-size: 16px;
|
|
742
|
+
font-weight: bold;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.cn-timeline-manage__composer-hint {
|
|
746
|
+
margin: 0;
|
|
747
|
+
color: var(--color-text-maxcontrast);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.cn-timeline-manage__draft-label {
|
|
751
|
+
font-weight: bold;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.cn-timeline-manage__draft {
|
|
755
|
+
width: 100%;
|
|
756
|
+
min-height: 70px;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.cn-timeline-manage__controls {
|
|
760
|
+
display: flex;
|
|
761
|
+
gap: 12px;
|
|
762
|
+
flex-wrap: wrap;
|
|
763
|
+
margin-block-end: 12px;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.cn-timeline-manage__filter {
|
|
767
|
+
min-width: 200px;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.cn-timeline-manage__list {
|
|
771
|
+
list-style: none;
|
|
772
|
+
margin: 0;
|
|
773
|
+
padding: 0;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.cn-timeline-manage__entry {
|
|
777
|
+
border-block-end: 1px solid var(--color-border);
|
|
778
|
+
padding-block: 12px;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.cn-timeline-manage__entry--pinned {
|
|
782
|
+
border-inline-start: 3px solid var(--color-primary-element);
|
|
783
|
+
padding-inline-start: 8px;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.cn-timeline-manage__head {
|
|
787
|
+
display: flex;
|
|
788
|
+
gap: 8px;
|
|
789
|
+
flex-wrap: wrap;
|
|
790
|
+
align-items: baseline;
|
|
791
|
+
color: var(--color-text-maxcontrast);
|
|
792
|
+
font-size: 0.9em;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.cn-timeline-manage__kind {
|
|
796
|
+
font-weight: bold;
|
|
797
|
+
color: var(--color-main-text);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.cn-timeline-manage__badge {
|
|
801
|
+
border: 1px solid var(--color-border);
|
|
802
|
+
border-radius: var(--border-radius);
|
|
803
|
+
padding: 0 6px;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.cn-timeline-manage__badge--open {
|
|
807
|
+
border-color: var(--color-warning);
|
|
808
|
+
color: var(--color-warning-text);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.cn-timeline-manage__message {
|
|
812
|
+
margin-block: 4px 0;
|
|
813
|
+
white-space: pre-wrap;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.cn-timeline-manage__siblings {
|
|
817
|
+
margin-block: 4px 0;
|
|
818
|
+
color: var(--color-text-maxcontrast);
|
|
819
|
+
font-size: 0.9em;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.cn-timeline-manage__actions {
|
|
823
|
+
display: flex;
|
|
824
|
+
gap: 8px;
|
|
825
|
+
margin-block-start: 4px;
|
|
826
|
+
}
|
|
827
|
+
</style>
|