@conduction/nextcloud-vue 2.143.0 → 2.145.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/CnActionDialog/CnActionDialog.vue.js +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +7 -2
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +1 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +55 -2
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +16 -0
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +3 -3
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
- package/dist/esm/components/CnEventsCard/CnEventsCard.vue.js +1 -1
- package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js +9 -0
- package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js.map +1 -1
- package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js +24 -13
- package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +2 -2
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +2 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +9 -4
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnMailTab/CnMailTab.vue.js +1 -1
- package/dist/esm/components/CnMailTab/CnMailTab.vue2.js.map +1 -1
- package/dist/esm/components/CnMailTab/CnMailTab.vue3.js +2 -2
- package/dist/esm/components/CnMailTab/CnMailTab.vue3.js.map +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +3 -0
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js +19 -14
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +30 -0
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +158 -102
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +73 -12
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +165 -126
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +12 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +1 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +14 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +2 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue.js +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +110 -2
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +146 -80
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -8
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue.js +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +10 -2
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +35 -9
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue.js +1 -1
- package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js +45 -0
- package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js +67 -34
- package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue.js +1 -1
- package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js +3 -22
- package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/calendar/eventStatus.js +67 -0
- package/dist/esm/integrations/builtin/calendar/eventStatus.js.map +1 -0
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +39 -0
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +81 -32
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue.js +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +5 -6
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/deckCardParts.js +59 -0
- package/dist/esm/integrations/builtin/deck/deckCardParts.js.map +1 -0
- package/dist/esm/l10n/en.json.js +6 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +8 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +1505 -637
- package/dist/esm/utils/builtInFormatters.js +18 -1
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/fetchFilterCounts.js +5 -2
- package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
- package/dist/esm/utils/localHeaderRows.js +95 -0
- package/dist/esm/utils/localHeaderRows.js.map +1 -0
- package/dist/esm/utils/nextcloudDirectory.js +19 -6
- package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +1848 -924
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1505 -637
- package/l10n/en.json +6 -0
- package/l10n/nl.json +8 -0
- package/package.json +1 -1
- package/src/components/CnActionDialog/CnActionDialog.vue +36 -4
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +55 -2
- package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +18 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +5 -2
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +2 -1
- package/src/components/CnEventsCard/CnEventsCard.vue +27 -5
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +2 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +15 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +9 -4
- package/src/components/CnMailTab/CnMailTab.vue +6 -4
- package/src/components/CnNotesCard/CnNotesCard.vue +8 -0
- package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +4 -1
- package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +114 -46
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +131 -20
- package/src/components/CnRetentionWidget/CnRetentionWidget.vue +26 -1
- package/src/components/CnSettingsField/CnSettingsField.vue +15 -0
- package/src/components/CnTasksWidget/CnTasksWidget.vue +170 -13
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -8
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +10 -2
- package/src/composables/indexSources.js +35 -9
- package/src/css/dialog.css +25 -10
- package/src/css/kpi-card.css +3 -0
- package/src/css/look-board-detail.css +513 -0
- package/src/css/look-board-index.css +52 -4
- package/src/css/look-board.css +163 -8
- package/src/css/table.css +8 -1
- package/src/integrations/builtin/calendar/CnCalendarCard.vue +76 -11
- package/src/integrations/builtin/calendar/CnCalendarTab.vue +3 -22
- package/src/integrations/builtin/calendar/__tests__/CnCalendarCardBoardRows.spec.js +52 -0
- package/src/integrations/builtin/calendar/eventStatus.js +63 -0
- package/src/integrations/builtin/deck/CnDeckCard.vue +66 -0
- package/src/integrations/builtin/deck/CnDeckTab.vue +5 -6
- package/src/integrations/builtin/deck/__tests__/CnDeckCardBoardRows.spec.js +49 -0
- package/src/integrations/builtin/deck/deckCardParts.js +56 -0
- package/src/utils/builtInFormatters.js +17 -0
- package/src/utils/fetchFilterCounts.js +5 -2
- package/src/utils/localHeaderRows.js +91 -0
- package/src/utils/nextcloudDirectory.js +19 -6
|
@@ -47,17 +47,42 @@
|
|
|
47
47
|
<table v-else class="cn-tasks-widget__table" data-testid="cn-tasks-widget-table">
|
|
48
48
|
<thead>
|
|
49
49
|
<tr>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
<!-- `content.sortable` / `content.filterable`: a head sorts by its
|
|
51
|
+
column and filters on its values (DqZaakTaken). -->
|
|
52
|
+
<th v-for="column in headColumns"
|
|
53
|
+
:key="column.key"
|
|
54
|
+
scope="col"
|
|
55
|
+
:aria-sort="headSort.key === column.key ? (headSort.dir === 'asc' ? 'ascending' : 'descending') : null"
|
|
56
|
+
:data-testid="`cn-tasks-widget-head-${column.key}`">
|
|
57
|
+
<span v-if="content.sortable === true || content.filterable === true" class="cn-tasks-widget__head">
|
|
58
|
+
<button v-if="content.sortable === true"
|
|
59
|
+
type="button"
|
|
60
|
+
class="cn-tasks-widget__head-sort"
|
|
61
|
+
:data-testid="`cn-tasks-widget-sort-${column.key}`"
|
|
62
|
+
@click="sortBy(column.key)">
|
|
63
|
+
{{ column.label }}
|
|
64
|
+
<ChevronUp v-if="headSort.key === column.key && headSort.dir === 'asc'" :size="14" />
|
|
65
|
+
<ChevronDown v-else :size="14" />
|
|
66
|
+
</button>
|
|
67
|
+
<template v-else>{{ column.label }}</template>
|
|
68
|
+
<NcActions v-if="content.filterable === true"
|
|
69
|
+
class="cn-tasks-widget__head-filter"
|
|
70
|
+
:aria-label="tr('Filter by {column}').replace('{column}', column.label)"
|
|
71
|
+
:data-testid="`cn-tasks-widget-filter-${column.key}`">
|
|
72
|
+
<template #icon>
|
|
73
|
+
<FilterOutline :size="14" />
|
|
74
|
+
</template>
|
|
75
|
+
<NcActionCheckbox v-for="option in headOptions(column.key)"
|
|
76
|
+
:key="option.value"
|
|
77
|
+
:modelValue="(headFilters[column.key] || []).includes(option.value)"
|
|
78
|
+
@update:modelValue="toggleHeadFilter(column.key, option.value)">
|
|
79
|
+
{{ option.label }}
|
|
80
|
+
</NcActionCheckbox>
|
|
81
|
+
</NcActions>
|
|
82
|
+
</span>
|
|
83
|
+
<template v-else>
|
|
84
|
+
{{ column.label }}
|
|
85
|
+
</template>
|
|
61
86
|
</th>
|
|
62
87
|
<th scope="col">
|
|
63
88
|
<span class="hidden-visually">{{ tr('Task actions') }}</span>
|
|
@@ -375,10 +400,13 @@ import axios from '@nextcloud/axios'
|
|
|
375
400
|
import { showError, showSuccess } from '@nextcloud/dialogs'
|
|
376
401
|
import { translate as t } from '@nextcloud/l10n'
|
|
377
402
|
import { generateUrl } from '@nextcloud/router'
|
|
378
|
-
import { NcActionButton, NcActions, NcAvatar, NcButton, NcLoadingIcon, NcTextField } from '@nextcloud/vue'
|
|
403
|
+
import { NcActionButton, NcActionCheckbox, NcActions, NcAvatar, NcButton, NcLoadingIcon, NcTextField } from '@nextcloud/vue'
|
|
379
404
|
import { inject, ref } from 'vue'
|
|
380
405
|
import CheckCircle from 'vue-material-design-icons/CheckCircle.vue'
|
|
406
|
+
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
|
|
407
|
+
import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
|
|
381
408
|
import CircleOutline from 'vue-material-design-icons/CircleOutline.vue'
|
|
409
|
+
import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
|
|
382
410
|
import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
|
|
383
411
|
import CnTaskFormDialog from '../CnTaskFormDialog/CnTaskFormDialog.vue'
|
|
384
412
|
import CnTaskWorkCard from './CnTaskWorkCard.vue'
|
|
@@ -458,12 +486,16 @@ export default {
|
|
|
458
486
|
|
|
459
487
|
components: {
|
|
460
488
|
CheckCircle,
|
|
489
|
+
ChevronDown,
|
|
490
|
+
ChevronUp,
|
|
461
491
|
CircleOutline,
|
|
462
492
|
CnStatusBadge,
|
|
463
493
|
CnTaskFormDialog,
|
|
464
494
|
CnTaskWorkCard,
|
|
495
|
+
FilterOutline,
|
|
465
496
|
NcActions,
|
|
466
497
|
NcActionButton,
|
|
498
|
+
NcActionCheckbox,
|
|
467
499
|
NcAvatar,
|
|
468
500
|
NcButton,
|
|
469
501
|
NcLoadingIcon,
|
|
@@ -618,6 +650,10 @@ export default {
|
|
|
618
650
|
data() {
|
|
619
651
|
return {
|
|
620
652
|
pollTimer: null,
|
|
653
|
+
/** The table's head sort, `{ key, dir }`; no key keeps open tasks first. */
|
|
654
|
+
headSort: { key: '', dir: 'asc' },
|
|
655
|
+
/** The table's head filters, `{ column: values[] }`. */
|
|
656
|
+
headFilters: {},
|
|
621
657
|
/** Uuids of the tasks a checkbox is completing, until the next read. */
|
|
622
658
|
completing: [],
|
|
623
659
|
/** Whether the table's create form is open. */
|
|
@@ -777,7 +813,26 @@ export default {
|
|
|
777
813
|
* @spec openspec/changes/record-task-table/specs/record-task-table/spec.md#requirement-a-records-tasks-show-as-a-table-with-their-summary-and-new-task-in-the-header
|
|
778
814
|
*/
|
|
779
815
|
tableRows() {
|
|
780
|
-
|
|
816
|
+
const kept = this.rows.filter((task) => Object.entries(this.headFilters).every(([key, values]) => values.length === 0 || values.includes(this.headValue(task, key))))
|
|
817
|
+
if (this.headSort.key) {
|
|
818
|
+
const dir = this.headSort.dir === 'desc' ? -1 : 1
|
|
819
|
+
return [...kept].sort((a, b) => {
|
|
820
|
+
const x = this.headSortValue(a, this.headSort.key)
|
|
821
|
+
const y = this.headSortValue(b, this.headSort.key)
|
|
822
|
+
return (x < y ? -1 : (x > y ? 1 : 0)) * dir
|
|
823
|
+
})
|
|
824
|
+
}
|
|
825
|
+
return [...kept.filter((task) => task.isTerminal !== true), ...kept.filter((task) => task.isTerminal === true)]
|
|
826
|
+
},
|
|
827
|
+
|
|
828
|
+
/** @return {Array<{key: string, label: string}>} The table's data columns. */
|
|
829
|
+
headColumns() {
|
|
830
|
+
return [
|
|
831
|
+
{ key: 'title', label: this.tr('Task') },
|
|
832
|
+
{ key: 'handler', label: this.tr('Handler') },
|
|
833
|
+
{ key: 'due', label: this.tr('Task deadline') },
|
|
834
|
+
{ key: 'state', label: this.tr('Status') },
|
|
835
|
+
]
|
|
781
836
|
},
|
|
782
837
|
|
|
783
838
|
/**
|
|
@@ -1158,6 +1213,84 @@ export default {
|
|
|
1158
1213
|
* @param {object} task The task row.
|
|
1159
1214
|
* @return {string} The name.
|
|
1160
1215
|
*/
|
|
1216
|
+
/**
|
|
1217
|
+
* A row's value in a column, as a head filter keeps it.
|
|
1218
|
+
*
|
|
1219
|
+
* @param {object} task The task row.
|
|
1220
|
+
* @param {string} key The column.
|
|
1221
|
+
* @return {string}
|
|
1222
|
+
*/
|
|
1223
|
+
headValue(task, key) {
|
|
1224
|
+
if (key === 'title') {
|
|
1225
|
+
return this.titleOf(task)
|
|
1226
|
+
}
|
|
1227
|
+
if (key === 'handler') {
|
|
1228
|
+
return String(task.assignee || '')
|
|
1229
|
+
}
|
|
1230
|
+
if (key === 'due') {
|
|
1231
|
+
return this.content.dueFormat === 'date' ? this.dueDate(task) : this.dueLabel(task)
|
|
1232
|
+
}
|
|
1233
|
+
return String(task.state || '')
|
|
1234
|
+
},
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* A row's value in a column, as its head sorts it: the deadline by date.
|
|
1238
|
+
*
|
|
1239
|
+
* @param {object} task The task row.
|
|
1240
|
+
* @param {string} key The column.
|
|
1241
|
+
* @return {string|number}
|
|
1242
|
+
*/
|
|
1243
|
+
headSortValue(task, key) {
|
|
1244
|
+
if (key === 'due') {
|
|
1245
|
+
const time = Date.parse(task.dueAt || task.dueDate || '')
|
|
1246
|
+
return Number.isNaN(time) ? Number.POSITIVE_INFINITY : time
|
|
1247
|
+
}
|
|
1248
|
+
if (key === 'handler') {
|
|
1249
|
+
return this.handlerName(task).toLowerCase()
|
|
1250
|
+
}
|
|
1251
|
+
if (key === 'state') {
|
|
1252
|
+
return this.stateLabel(task).toLowerCase()
|
|
1253
|
+
}
|
|
1254
|
+
return this.titleOf(task).toLowerCase()
|
|
1255
|
+
},
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* The values a column's head filter offers, one per value the rows hold.
|
|
1259
|
+
*
|
|
1260
|
+
* @param {string} key The column.
|
|
1261
|
+
* @return {Array<{value: string, label: string}>}
|
|
1262
|
+
*/
|
|
1263
|
+
headOptions(key) {
|
|
1264
|
+
const seen = new Map()
|
|
1265
|
+
for (const task of this.rows) {
|
|
1266
|
+
const value = this.headValue(task, key)
|
|
1267
|
+
if (value !== '' && !seen.has(value)) {
|
|
1268
|
+
seen.set(value, key === 'handler' ? this.handlerName(task) : (key === 'state' ? this.stateLabel(task) : value))
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return [...seen].map(([value, label]) => ({ value, label }))
|
|
1272
|
+
},
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Sort by a column: ascending first, then descending.
|
|
1276
|
+
*
|
|
1277
|
+
* @param {string} key The column.
|
|
1278
|
+
*/
|
|
1279
|
+
sortBy(key) {
|
|
1280
|
+
this.headSort = this.headSort.key === key && this.headSort.dir === 'asc' ? { key, dir: 'desc' } : { key, dir: 'asc' }
|
|
1281
|
+
},
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Keep or drop one value of a column's head filter.
|
|
1285
|
+
*
|
|
1286
|
+
* @param {string} key The column.
|
|
1287
|
+
* @param {string} value The value.
|
|
1288
|
+
*/
|
|
1289
|
+
toggleHeadFilter(key, value) {
|
|
1290
|
+
const current = this.headFilters[key] || []
|
|
1291
|
+
this.headFilters = { ...this.headFilters, [key]: current.includes(value) ? current.filter((v) => v !== value) : [...current, value] }
|
|
1292
|
+
},
|
|
1293
|
+
|
|
1161
1294
|
handlerName(task) {
|
|
1162
1295
|
return userDisplayName(task.assignee) || String(task.assignee || '')
|
|
1163
1296
|
},
|
|
@@ -1594,6 +1727,30 @@ function recordUuid(configured, objectId) {
|
|
|
1594
1727
|
font-size: 14px;
|
|
1595
1728
|
}
|
|
1596
1729
|
|
|
1730
|
+
.cn-tasks-widget__head {
|
|
1731
|
+
display: inline-flex;
|
|
1732
|
+
align-items: center;
|
|
1733
|
+
gap: 4px;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
.cn-tasks-widget__head-sort {
|
|
1737
|
+
display: inline-flex;
|
|
1738
|
+
align-items: center;
|
|
1739
|
+
gap: 4px;
|
|
1740
|
+
min-height: 0;
|
|
1741
|
+
margin: 0;
|
|
1742
|
+
padding: 0;
|
|
1743
|
+
border: 0;
|
|
1744
|
+
background: transparent;
|
|
1745
|
+
color: inherit;
|
|
1746
|
+
font: inherit;
|
|
1747
|
+
cursor: pointer;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
.cn-tasks-widget__head-filter {
|
|
1751
|
+
--button-size: 22px;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1597
1754
|
.cn-tasks-widget__table th {
|
|
1598
1755
|
padding: 12px 16px;
|
|
1599
1756
|
background: color-mix(in srgb, var(--color-background-hover) 40%, var(--color-main-background));
|
|
@@ -118,9 +118,10 @@ import CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'
|
|
|
118
118
|
import CnDataTable from '../CnDataTable/CnDataTable.vue'
|
|
119
119
|
import { getByPath, useEndpointSource } from '../../composables/useEndpointSource.js'
|
|
120
120
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
121
|
-
import { dispatchAction, resolveObjectOpType } from '../../utils/actionsDispatcher.js'
|
|
121
|
+
import { dispatchAction, interpolateActionTarget, resolveObjectOpType } from '../../utils/actionsDispatcher.js'
|
|
122
122
|
import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
|
|
123
123
|
import { headerFilterParams, nextHeaderFilters, withSortableColumns } from '../../utils/headerFilterState.js'
|
|
124
|
+
import { localHeaderRows } from '../../utils/localHeaderRows.js'
|
|
124
125
|
import { pageFilter, pageSort } from '../../utils/pageControls.js'
|
|
125
126
|
import { readPath } from '../../utils/readPath.js'
|
|
126
127
|
import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
@@ -869,9 +870,9 @@ export default {
|
|
|
869
870
|
if (this.endpointActive && this.columns.some((col) => col && typeof col === 'object' && col.labelPath)) {
|
|
870
871
|
inner.columns = this.labelledColumns(inner.columns || this.columns)
|
|
871
872
|
}
|
|
872
|
-
// Header sort and filter (`sortable`, `filterable`)
|
|
873
|
-
//
|
|
874
|
-
if (this.selfFetchActive && (this.sortable !== false || this.filterable)) {
|
|
873
|
+
// Header sort and filter (`sortable`, `filterable`): the server sorts
|
|
874
|
+
// and filters a self-fetch; endpoint rows are sorted and filtered here.
|
|
875
|
+
if ((this.selfFetchActive || this.endpointActive) && (this.sortable !== false || this.filterable)) {
|
|
875
876
|
inner.columns = withSortableColumns(inner.columns || this.columns, this.sortable)
|
|
876
877
|
inner.filterable = this.filterable
|
|
877
878
|
inner.activeFilters = this.headerFilters
|
|
@@ -889,7 +890,7 @@ export default {
|
|
|
889
890
|
}
|
|
890
891
|
}
|
|
891
892
|
if (this.endpointActive) {
|
|
892
|
-
inner.rows = this.endpointRows
|
|
893
|
+
inner.rows = localHeaderRows(this.endpointRows, this.headerSort, this.headerFilters)
|
|
893
894
|
inner.loading = this.epLoading || this.loading
|
|
894
895
|
}
|
|
895
896
|
if (this.rowRoute) {
|
|
@@ -1159,9 +1160,13 @@ export default {
|
|
|
1159
1160
|
return { ok: false, error: '' }
|
|
1160
1161
|
}
|
|
1161
1162
|
if (action.type !== 'object-op') {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
: action
|
|
1163
|
+
let wrapped = action
|
|
1164
|
+
if (!action.type || action.type === 'handler') {
|
|
1165
|
+
wrapped = { ...action, args: [...(action.args || []), row] }
|
|
1166
|
+
} else if (action.type === 'navigate' && row) {
|
|
1167
|
+
// A row's navigate reads that row: `{objectId}` / `@objectId` is its id, `@object.<field>` its fields.
|
|
1168
|
+
wrapped = { ...action, target: interpolateActionTarget(action.target, { objectId: row.id, object: row }) }
|
|
1169
|
+
}
|
|
1165
1170
|
if (typeof this.cnDispatchAction === 'function') {
|
|
1166
1171
|
this.cnDispatchAction(wrapped)
|
|
1167
1172
|
} else {
|
|
@@ -880,15 +880,23 @@ export default {
|
|
|
880
880
|
*/
|
|
881
881
|
/**
|
|
882
882
|
* The value an input starts with: `content.default`, else the current
|
|
883
|
-
* quarter or year for those types, else empty.
|
|
883
|
+
* quarter or year for those types, else empty. For a quarter or year
|
|
884
|
+
* input, `default: "previous"` is the last complete one (DqTermijnen
|
|
885
|
+
* opens on the last complete quarter).
|
|
884
886
|
*
|
|
885
887
|
* @return {string}
|
|
886
888
|
*/
|
|
887
889
|
inputDefault() {
|
|
890
|
+
const today = new Date()
|
|
891
|
+
if (this.content.default === 'previous' && this.inputType === 'quarter') {
|
|
892
|
+
return quarterKey(new Date(today.getFullYear(), today.getMonth() - 3, 1))
|
|
893
|
+
}
|
|
894
|
+
if (this.content.default === 'previous' && this.inputType === 'year') {
|
|
895
|
+
return String(today.getFullYear() - 1)
|
|
896
|
+
}
|
|
888
897
|
if (this.content.default !== undefined && this.content.default !== null) {
|
|
889
898
|
return String(this.content.default)
|
|
890
899
|
}
|
|
891
|
-
const today = new Date()
|
|
892
900
|
if (this.inputType === 'quarter') {
|
|
893
901
|
return quarterKey(today)
|
|
894
902
|
}
|
|
@@ -1109,7 +1109,9 @@ function fillRowTemplate(template, row) {
|
|
|
1109
1109
|
* template, "{case.title}") or `linkedField` (a path) fills the "Linked"
|
|
1110
1110
|
* column, drawn when a kind declares one; `sourceConfig.linkedLabel` names it.
|
|
1111
1111
|
* `subtitleTemplate` ("Case {identifier}", translated) fills the line under a
|
|
1112
|
-
* result, else `subtitleField
|
|
1112
|
+
* result, else `subtitleField`; `subtitleResolve` (`{ field, schema, labelField,
|
|
1113
|
+
* as }`) names a referenced object for it as `{as}`. `sourceConfig.titleWrap`
|
|
1114
|
+
* wraps a long result title instead of cutting it.
|
|
1113
1115
|
* `linkedUser: true` reads that value as a user id and shows the user's
|
|
1114
1116
|
* display name. `sourceConfig.columnWidths` sets a width per column key.
|
|
1115
1117
|
* The page's title and count line can name the term as `{term}`, and the
|
|
@@ -1226,9 +1228,32 @@ function searchSource(config = {}, context = {}) {
|
|
|
1226
1228
|
return ''
|
|
1227
1229
|
}
|
|
1228
1230
|
|
|
1229
|
-
|
|
1231
|
+
// `subtitleResolve`: `{ field, schema, register?, labelField?, as }` names
|
|
1232
|
+
// the object a reference points at, for the subtitle template as `{as}`.
|
|
1233
|
+
const subtitleLabels = async (kind) => {
|
|
1234
|
+
const resolve = kind.subtitleResolve
|
|
1235
|
+
if (!resolve || !resolve.schema || !resolve.field) {
|
|
1236
|
+
return null
|
|
1237
|
+
}
|
|
1238
|
+
const objects = await fetchReferenceObjects({ register: resolve.register || kind.register, schema: resolve.schema }).catch(() => [])
|
|
1239
|
+
const labels = new Map()
|
|
1240
|
+
for (const object of objects) {
|
|
1241
|
+
const id = object.id || object.uuid || (object['@self'] && object['@self'].id)
|
|
1242
|
+
const label = readPath(object, resolve.labelField || 'title') ?? object.name
|
|
1243
|
+
if (id !== undefined && label !== undefined && label !== null) {
|
|
1244
|
+
labels.set(String(id), String(label))
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return labels
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const subtitleOf = (kind, object, labels = null) => {
|
|
1230
1251
|
if (kind.subtitleTemplate) {
|
|
1231
|
-
|
|
1252
|
+
const resolve = kind.subtitleResolve
|
|
1253
|
+
const data = labels && resolve
|
|
1254
|
+
? { ...object, [resolve.as || resolve.field]: labels.get(String(readPath(object, resolve.field) ?? '')) ?? '' }
|
|
1255
|
+
: object
|
|
1256
|
+
return fillTemplate(translate(kind.subtitleTemplate), data)
|
|
1232
1257
|
}
|
|
1233
1258
|
return kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : ''
|
|
1234
1259
|
}
|
|
@@ -1247,6 +1272,7 @@ function searchSource(config = {}, context = {}) {
|
|
|
1247
1272
|
const results = Array.isArray(body.results) ? body.results : (Array.isArray(body) ? body : [])
|
|
1248
1273
|
const labels = kind.statusResolve && kind.statusField ? await statusLabels(kind) : null
|
|
1249
1274
|
const userNames = kind.linkedUser === true ? await linkedUserNames(kind, results) : null
|
|
1275
|
+
const subtitleNames = await subtitleLabels(kind)
|
|
1250
1276
|
return {
|
|
1251
1277
|
total: typeof body.total === 'number' ? body.total : results.length,
|
|
1252
1278
|
rows: results.map((object) => {
|
|
@@ -1261,7 +1287,7 @@ function searchSource(config = {}, context = {}) {
|
|
|
1261
1287
|
kindLabel: kind.label,
|
|
1262
1288
|
kindText: kindTextOf(kind, object),
|
|
1263
1289
|
title: String(readPath(object, kind.titleField || 'title') ?? object.name ?? id ?? ''),
|
|
1264
|
-
subtitle: subtitleOf(kind, object),
|
|
1290
|
+
subtitle: subtitleOf(kind, object, subtitleNames),
|
|
1265
1291
|
status,
|
|
1266
1292
|
linked: userNames ? (userNames.get(linkedOf(kind, object)) || linkedOf(kind, object)) : linkedOf(kind, object),
|
|
1267
1293
|
href: kind.href ? generateUrl(fillRowTemplate(kind.href, { ...object, id })) : '',
|
|
@@ -1372,7 +1398,7 @@ function searchSource(config = {}, context = {}) {
|
|
|
1372
1398
|
loading: () => state.loading,
|
|
1373
1399
|
|
|
1374
1400
|
columns: withColumnWidths([
|
|
1375
|
-
{ key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle', sortable: true },
|
|
1401
|
+
{ key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle', sortable: true, ...(config && config.titleWrap === true ? { cellClass: 'cn-cell--wrap' } : {}) },
|
|
1376
1402
|
// `kindAsText`: what one row is, as plain text ("Zaak", "Lantaarnpaal",
|
|
1377
1403
|
// DqZoekresultaten), instead of the plural kind as a pill.
|
|
1378
1404
|
config && config.kindAsText === true
|
|
@@ -1619,10 +1645,10 @@ function usersSource() {
|
|
|
1619
1645
|
loading: () => state.loading,
|
|
1620
1646
|
|
|
1621
1647
|
columns: [
|
|
1622
|
-
{ key: 'displayname', label: t('nextcloud-vue', 'Employee'), secondary: 'function' },
|
|
1623
|
-
{ key: 'roles', label: t('nextcloud-vue', 'Roles') },
|
|
1624
|
-
{ key: 'email', label: t('nextcloud-vue', 'E-mail') },
|
|
1625
|
-
{ key: 'today', label: t('nextcloud-vue', 'Today'), widget: 'badge', widgetProps: { colorMap: presenceColors, variant: 'warning' } },
|
|
1648
|
+
{ key: 'displayname', label: t('nextcloud-vue', 'Employee'), secondary: 'function', sortable: true, cellClass: 'cn-cell--lead' },
|
|
1649
|
+
{ key: 'roles', label: t('nextcloud-vue', 'Roles'), sortable: true },
|
|
1650
|
+
{ key: 'email', label: t('nextcloud-vue', 'E-mail'), sortable: true },
|
|
1651
|
+
{ key: 'today', label: t('nextcloud-vue', 'Today'), widget: 'badge', widgetProps: { colorMap: presenceColors, variant: 'warning' }, sortable: true },
|
|
1626
1652
|
],
|
|
1627
1653
|
|
|
1628
1654
|
searchFields: {
|
package/src/css/dialog.css
CHANGED
|
@@ -177,11 +177,14 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
|
|
|
177
177
|
color: var(--color-text-maxcontrast);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
/*
|
|
181
|
-
|
|
180
|
+
/* A body starts 22px under the subtitle, as the Dq dialog boards draw a
|
|
181
|
+
first field: the header's foot and the body gap alone, so the saved-view
|
|
182
|
+
edit and share forms and the confirm sentence pad nothing on top
|
|
183
|
+
(r29 on :8080: 28, 28 and 26px with their own padding). */
|
|
182
184
|
.cn-dialog-board .dialog__content .cn-edit-view,
|
|
183
|
-
.cn-dialog-board .dialog__content .cn-share-view
|
|
184
|
-
|
|
185
|
+
.cn-dialog-board .dialog__content .cn-share-view,
|
|
186
|
+
.cn-dialog-board .dialog__content .cn-confirm__confirm {
|
|
187
|
+
padding-top: 0;
|
|
185
188
|
}
|
|
186
189
|
|
|
187
190
|
/* The close button is a 36px circle. NcButton sizes an icon-only button by
|
|
@@ -216,9 +219,17 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
|
|
|
216
219
|
/* A choice card with a checkbox (CnAssignDialog's toggles) is as tight as
|
|
217
220
|
the board draws it (DqOverdragen, 76px for a label and a two-line note):
|
|
218
221
|
10px 14px padding, the checkbox row at the 24px minimum target instead of
|
|
219
|
-
NcCheckboxRadioSwitch's 34px, the note at 1.2 line height
|
|
220
|
-
|
|
222
|
+
NcCheckboxRadioSwitch's 34px, the note at 1.2 line height; the card's
|
|
223
|
+
corners 8px and the label on an 18px line, as the board draws them. */
|
|
224
|
+
/* `.dialog` outranks the component's scoped rule (class + attribute), which
|
|
225
|
+
loads after this sheet and kept 12px 14px and 12px corners on :8080 (r29). */
|
|
226
|
+
.cn-dialog-board .dialog .cn-assign-dialog__toggle {
|
|
221
227
|
padding: 10px 14px;
|
|
228
|
+
border-radius: 8px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.cn-dialog-board .cn-assign-dialog__toggle .checkbox-content__text {
|
|
232
|
+
line-height: 18px;
|
|
222
233
|
}
|
|
223
234
|
|
|
224
235
|
.cn-dialog-board .cn-assign-dialog__toggle .checkbox-radio-switch-checkbox .checkbox-content,
|
|
@@ -341,10 +352,14 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
|
|
|
341
352
|
border and radius 8 (PlanItemDialog, DqWerkvoorraad). NcInputField draws
|
|
342
353
|
its border as a 65%-transparent shadow, which on the white dialog read as
|
|
343
354
|
an empty band, and floats its label inside the border. A field with
|
|
344
|
-
`labelOutside` keeps the app's own label and takes the same control.
|
|
355
|
+
`labelOutside` keeps the app's own label and takes the same control. The
|
|
356
|
+
label's box starts where the field starts and the control 23px under it,
|
|
357
|
+
as on every dialog board (72 fields on 11 boards): the label at the font's
|
|
358
|
+
normal line, 21px of room plus the control's 2px focus inset, the label
|
|
359
|
+
4px above that room's end. */
|
|
345
360
|
.cn-dialog-board .dialog__content .input-field:has(.input-field__label),
|
|
346
361
|
.cn-dialog-board .dialog__content .textarea:has(.textarea__label) {
|
|
347
|
-
margin-block-start:
|
|
362
|
+
margin-block-start: 21px;
|
|
348
363
|
}
|
|
349
364
|
|
|
350
365
|
/* NcSelect's search is an NcInputField inside the select, and the select's
|
|
@@ -353,7 +368,7 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
|
|
|
353
368
|
select makes the label's room instead.
|
|
354
369
|
@spec openspec/changes/board-dialog-select-label-room/specs/board-dialog-select-label-room/spec.md#requirement-a-board-dialog-select-makes-room-for-its-raised-label */
|
|
355
370
|
.cn-dialog-board .dialog__content .v-select:has(.input-field__label) {
|
|
356
|
-
margin-block-start:
|
|
371
|
+
margin-block-start: 21px;
|
|
357
372
|
}
|
|
358
373
|
|
|
359
374
|
/* A select in a form dialog sits under its label and over its help line
|
|
@@ -414,7 +429,7 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
|
|
|
414
429
|
color: var(--color-main-text);
|
|
415
430
|
font-size: var(--cn-field-label-size, 14px);
|
|
416
431
|
font-weight: 600;
|
|
417
|
-
line-height:
|
|
432
|
+
line-height: normal;
|
|
418
433
|
pointer-events: auto;
|
|
419
434
|
transition: none;
|
|
420
435
|
}
|
package/src/css/kpi-card.css
CHANGED
|
@@ -547,6 +547,9 @@ a.cn-kpi-card--board * {
|
|
|
547
547
|
.cn-kpi-card--board.cn-kpi-card--stacked:has(.cn-kpi-card__title--badge-end) {
|
|
548
548
|
--cn-kpi-stacked-padding: 20px 22px;
|
|
549
549
|
--cn-kpi-stacked-gap: 8px;
|
|
550
|
+
/* Its label is set at 600, as every board draws this tile (DqDashboard,
|
|
551
|
+
DqMijnTeam, DqPersoonlijkeInstellingen); the plain stacked tile keeps 400. */
|
|
552
|
+
--cn-kpi-stacked-title-weight: 600;
|
|
550
553
|
}
|
|
551
554
|
|
|
552
555
|
.cn-kpi-card--board.cn-kpi-card--stacked:has(.cn-kpi-card__title--badge-end) .cn-kpi-card__value {
|