@conduction/nextcloud-vue 0.1.0-beta.1 → 0.1.0-beta.10
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/README.md +226 -0
- package/css/index.css +5 -0
- package/dist/nextcloud-vue.cjs +67614 -0
- package/dist/nextcloud-vue.cjs.js +76311 -5905
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.cjs.map +1 -0
- package/dist/nextcloud-vue.css +3279 -203
- package/dist/nextcloud-vue.esm.js +76240 -5882
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +89 -63
- package/src/components/CnActionsBar/CnActionsBar.vue +254 -0
- package/src/components/CnActionsBar/index.js +1 -0
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +569 -0
- package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +422 -0
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
- package/src/components/CnAdvancedFormDialog/index.js +1 -0
- package/src/components/CnCard/CnCard.vue +415 -0
- package/src/components/CnCard/index.js +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +23 -20
- package/src/components/CnCardGrid/index.js +1 -1
- package/src/components/CnCellRenderer/index.js +1 -1
- package/src/components/CnChartWidget/CnChartWidget.vue +318 -0
- package/src/components/CnChartWidget/index.js +1 -0
- package/src/components/CnConfigurationCard/index.js +1 -1
- package/src/components/CnContextMenu/CnContextMenu.vue +142 -0
- package/src/components/CnContextMenu/index.js +1 -0
- package/src/components/CnCopyDialog/CnCopyDialog.vue +257 -0
- package/src/components/CnCopyDialog/index.js +1 -0
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -0
- package/src/components/CnDashboardGrid/index.js +1 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +396 -0
- package/src/components/CnDashboardPage/index.js +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +24 -16
- package/src/components/CnDataTable/index.js +1 -1
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -0
- package/src/components/CnDeleteDialog/index.js +1 -0
- package/src/components/CnDetailCard/CnDetailCard.vue +225 -0
- package/src/components/CnDetailCard/index.js +1 -0
- package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
- package/src/components/CnDetailGrid/index.js +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +431 -0
- package/src/components/CnDetailPage/index.js +1 -0
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +12 -2
- package/src/components/CnFacetSidebar/index.js +1 -1
- package/src/components/CnFilterBar/index.js +1 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +934 -0
- package/src/components/CnFormDialog/index.js +1 -0
- package/src/components/CnIcon/CnIcon.vue +89 -0
- package/src/components/CnIcon/index.js +1 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +589 -291
- package/src/components/CnIndexPage/index.js +1 -1
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +535 -0
- package/src/components/CnIndexSidebar/index.js +1 -0
- package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
- package/src/components/CnInfoWidget/index.js +1 -0
- package/src/components/CnItemCard/CnItemCard.vue +134 -0
- package/src/components/CnItemCard/index.js +1 -0
- package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
- package/src/components/CnJsonViewer/index.js +1 -0
- package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
- package/src/components/CnKpiGrid/index.js +1 -1
- package/src/components/CnMassActionBar/CnMassActionBar.vue +6 -5
- package/src/components/CnMassActionBar/index.js +1 -1
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +16 -9
- package/src/components/CnMassCopyDialog/index.js +1 -1
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +16 -9
- package/src/components/CnMassDeleteDialog/index.js +1 -1
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +8 -7
- package/src/components/CnMassExportDialog/index.js +1 -1
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +20 -17
- package/src/components/CnMassImportDialog/index.js +1 -1
- package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
- package/src/components/CnNoteCard/index.js +1 -0
- package/src/components/CnNotesCard/CnNotesCard.vue +415 -0
- package/src/components/CnNotesCard/index.js +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +3 -1
- package/src/components/CnObjectCard/index.js +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +853 -0
- package/src/components/CnObjectDataWidget/index.js +1 -0
- package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +288 -0
- package/src/components/CnObjectMetadataWidget/index.js +1 -0
- package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
- package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +254 -0
- package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
- package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
- package/src/components/CnObjectSidebar/index.js +6 -0
- package/src/components/CnPageHeader/CnPageHeader.vue +61 -0
- package/src/components/CnPageHeader/index.js +1 -0
- package/src/components/CnPagination/CnPagination.vue +7 -6
- package/src/components/CnPagination/index.js +1 -1
- package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
- package/src/components/CnProgressBar/index.js +1 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -0
- package/src/components/CnRegisterMapping/index.js +1 -0
- package/src/components/CnRowActions/CnRowActions.vue +25 -3
- package/src/components/CnRowActions/index.js +1 -1
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
- package/src/components/CnSchemaFormDialog/index.js +1 -0
- package/src/components/CnSettingsCard/index.js +1 -1
- package/src/components/CnSettingsSection/index.js +1 -1
- package/src/components/CnStatsBlock/CnStatsBlock.vue +89 -19
- package/src/components/CnStatsBlock/index.js +1 -1
- package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
- package/src/components/CnStatsPanel/index.js +1 -0
- package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
- package/src/components/CnStatusBadge/index.js +1 -1
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +544 -0
- package/src/components/CnTabbedFormDialog/index.js +1 -0
- package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
- package/src/components/CnTableWidget/index.js +1 -0
- package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
- package/src/components/CnTasksCard/index.js +1 -0
- package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
- package/src/components/CnTileWidget/index.js +1 -0
- package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
- package/src/components/CnTimelineStages/index.js +1 -0
- package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
- package/src/components/CnUserActionMenu/index.js +1 -0
- package/src/components/CnVersionInfoCard/index.js +1 -1
- package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
- package/src/components/CnWidgetRenderer/index.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +246 -0
- package/src/components/CnWidgetWrapper/index.js +1 -0
- package/src/components/index.js +57 -25
- package/src/composables/index.js +5 -3
- package/src/composables/useContextMenu.js +126 -0
- package/src/composables/useDashboardView.js +286 -0
- package/src/composables/useDetailView.js +290 -132
- package/src/composables/useListView.js +364 -153
- package/src/composables/useSubResource.js +142 -142
- package/src/constants/metadata.js +30 -0
- package/src/css/CnSchemaFormDialog.css +546 -0
- package/src/css/__sample_nextcloud_tokens.css +110 -0
- package/src/css/actions-bar.css +54 -0
- package/src/css/badge.css +83 -51
- package/src/css/card.css +129 -128
- package/src/css/context-menu.css +20 -0
- package/src/css/dashboard.css +70 -0
- package/src/css/detail-page.css +235 -0
- package/src/css/detail.css +68 -68
- package/src/css/index-page.css +44 -0
- package/src/css/index-sidebar.css +193 -0
- package/src/css/index.css +17 -8
- package/src/css/layout.css +90 -90
- package/src/css/page-header.css +35 -0
- package/src/css/pagination.css +72 -72
- package/src/css/table.css +142 -143
- package/src/css/timeline-stages.css +220 -0
- package/src/css/utilities.css +46 -46
- package/src/index.js +91 -50
- package/src/mixins/gridLayout.js +118 -0
- package/src/store/createCrudStore.js +360 -0
- package/src/store/createSubResourcePlugin.js +125 -135
- package/src/store/index.js +4 -3
- package/src/store/plugins/auditTrails.js +357 -17
- package/src/store/plugins/files.js +250 -186
- package/src/store/plugins/index.js +7 -4
- package/src/store/plugins/lifecycle.js +180 -180
- package/src/store/plugins/registerMapping.js +195 -0
- package/src/store/plugins/relations.js +68 -68
- package/src/store/plugins/search.js +385 -0
- package/src/store/plugins/selection.js +104 -0
- package/src/store/useObjectStore.js +823 -625
- package/src/types/auditTrail.d.ts +32 -32
- package/src/types/file.d.ts +23 -23
- package/src/types/index.d.ts +35 -35
- package/src/types/notification.d.ts +36 -36
- package/src/types/object.d.ts +40 -40
- package/src/types/organisation.d.ts +41 -41
- package/src/types/register.d.ts +25 -25
- package/src/types/schema.d.ts +39 -39
- package/src/types/shared.d.ts +79 -79
- package/src/types/source.d.ts +14 -14
- package/src/types/task.d.ts +31 -31
- package/src/utils/errors.js +96 -96
- package/src/utils/getTheme.js +9 -0
- package/src/utils/headers.js +80 -44
- package/src/utils/id.js +13 -0
- package/src/utils/index.js +4 -3
- package/src/utils/schema.js +422 -287
- package/src/utils/widgetVisibility.js +162 -0
- package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
- package/src/components/CnDetailViewLayout/index.js +0 -1
- package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
- package/src/components/CnEmptyState/index.js +0 -1
- package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
- package/src/components/CnListViewLayout/index.js +0 -1
- package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
- package/src/components/CnViewModeToggle/index.js +0 -1
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
CnTableWidget — Data table widget with card wrapper and dual data sourcing.
|
|
3
|
+
|
|
4
|
+
Wraps CnDataTable in a card container with a title header, optional "View all"
|
|
5
|
+
footer link, and loading/empty states. Supports two data modes:
|
|
6
|
+
1. External: `rows` prop provided (no API calls)
|
|
7
|
+
2. Self-fetch: `register` + `schemaId` provided (fetches from OpenRegister API)
|
|
8
|
+
|
|
9
|
+
Used in dashboard and detail page grid layouts for displaying related data tables.
|
|
10
|
+
-->
|
|
11
|
+
<template>
|
|
12
|
+
<div class="cn-table-widget">
|
|
13
|
+
<!-- Header -->
|
|
14
|
+
<div v-if="title" class="cn-table-widget__header">
|
|
15
|
+
<h3 class="cn-table-widget__title">
|
|
16
|
+
{{ title }}
|
|
17
|
+
</h3>
|
|
18
|
+
<span v-if="totalCount > 0" class="cn-table-widget__count">
|
|
19
|
+
{{ totalCount }}
|
|
20
|
+
</span>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- Loading state -->
|
|
24
|
+
<NcLoadingIcon v-if="isLoading" class="cn-table-widget__loading" :size="32" />
|
|
25
|
+
|
|
26
|
+
<!-- Empty state -->
|
|
27
|
+
<p v-else-if="displayRows.length === 0" class="cn-table-widget__empty">
|
|
28
|
+
{{ emptyText }}
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
<!-- Data table -->
|
|
32
|
+
<CnDataTable
|
|
33
|
+
v-else
|
|
34
|
+
:rows="displayRows"
|
|
35
|
+
:columns="columns"
|
|
36
|
+
:loading="false"
|
|
37
|
+
:selectable="false"
|
|
38
|
+
@row-click="onRowClick" />
|
|
39
|
+
|
|
40
|
+
<!-- Footer with view all link -->
|
|
41
|
+
<div
|
|
42
|
+
v-if="viewAllRoute && totalCount > limitedCount"
|
|
43
|
+
class="cn-table-widget__footer">
|
|
44
|
+
<a
|
|
45
|
+
class="cn-table-widget__view-all"
|
|
46
|
+
@click.prevent="$router.push(viewAllRoute)">
|
|
47
|
+
{{ viewAllLabel }}
|
|
48
|
+
</a>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script>
|
|
54
|
+
import { NcLoadingIcon } from '@nextcloud/vue'
|
|
55
|
+
import CnDataTable from '../CnDataTable/CnDataTable.vue'
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* CnTableWidget — Data table widget with card wrapper and dual data sourcing.
|
|
59
|
+
*
|
|
60
|
+
* @example External data mode
|
|
61
|
+
* <CnTableWidget
|
|
62
|
+
* title="Related Skills"
|
|
63
|
+
* :rows="skillRows"
|
|
64
|
+
* :columns="skillColumns"
|
|
65
|
+
* :view-all-route="{ name: 'Skills' }" />
|
|
66
|
+
*
|
|
67
|
+
* @example Self-fetch mode
|
|
68
|
+
* <CnTableWidget
|
|
69
|
+
* title="Documents"
|
|
70
|
+
* register="9"
|
|
71
|
+
* schema-id="42"
|
|
72
|
+
* :limit="5" />
|
|
73
|
+
*/
|
|
74
|
+
export default {
|
|
75
|
+
name: 'CnTableWidget',
|
|
76
|
+
|
|
77
|
+
components: {
|
|
78
|
+
NcLoadingIcon,
|
|
79
|
+
CnDataTable,
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
props: {
|
|
83
|
+
/** Widget title shown in the header. */
|
|
84
|
+
title: {
|
|
85
|
+
type: String,
|
|
86
|
+
default: '',
|
|
87
|
+
},
|
|
88
|
+
/**
|
|
89
|
+
* External row data. When provided, no API calls are made.
|
|
90
|
+
*
|
|
91
|
+
* @type {object[]}
|
|
92
|
+
*/
|
|
93
|
+
rows: {
|
|
94
|
+
type: Array,
|
|
95
|
+
default: null,
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* Column definitions for CnDataTable.
|
|
99
|
+
*
|
|
100
|
+
* @type {{ key: string, label: string, sortable?: boolean }[]}
|
|
101
|
+
*/
|
|
102
|
+
columns: {
|
|
103
|
+
type: Array,
|
|
104
|
+
default: () => [],
|
|
105
|
+
},
|
|
106
|
+
/**
|
|
107
|
+
* OpenRegister register ID for self-fetch mode.
|
|
108
|
+
*
|
|
109
|
+
* @type {string|number}
|
|
110
|
+
*/
|
|
111
|
+
register: {
|
|
112
|
+
type: [String, Number],
|
|
113
|
+
default: null,
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* OpenRegister schema ID for self-fetch mode.
|
|
117
|
+
*
|
|
118
|
+
* @type {string|number}
|
|
119
|
+
*/
|
|
120
|
+
schemaId: {
|
|
121
|
+
type: [String, Number],
|
|
122
|
+
default: null,
|
|
123
|
+
},
|
|
124
|
+
/**
|
|
125
|
+
* Maximum number of rows to display. When total exceeds this,
|
|
126
|
+
* a "View all" link appears.
|
|
127
|
+
*
|
|
128
|
+
* @type {number}
|
|
129
|
+
*/
|
|
130
|
+
limit: {
|
|
131
|
+
type: Number,
|
|
132
|
+
default: 0,
|
|
133
|
+
},
|
|
134
|
+
/**
|
|
135
|
+
* Vue Router route for the "View all" link.
|
|
136
|
+
*
|
|
137
|
+
* @type {object}
|
|
138
|
+
*/
|
|
139
|
+
viewAllRoute: {
|
|
140
|
+
type: Object,
|
|
141
|
+
default: null,
|
|
142
|
+
},
|
|
143
|
+
/**
|
|
144
|
+
* Function that returns a route object for row click navigation.
|
|
145
|
+
* Receives the row data as argument.
|
|
146
|
+
*
|
|
147
|
+
* @type {Function}
|
|
148
|
+
*/
|
|
149
|
+
rowClickRoute: {
|
|
150
|
+
type: Function,
|
|
151
|
+
default: null,
|
|
152
|
+
},
|
|
153
|
+
/** Pre-translated "View all" label. */
|
|
154
|
+
viewAllLabel: {
|
|
155
|
+
type: String,
|
|
156
|
+
default: 'View all',
|
|
157
|
+
},
|
|
158
|
+
/** Pre-translated empty state text. */
|
|
159
|
+
emptyText: {
|
|
160
|
+
type: String,
|
|
161
|
+
default: 'No data available',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
data() {
|
|
166
|
+
return {
|
|
167
|
+
fetchedRows: [],
|
|
168
|
+
loading: false,
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
computed: {
|
|
173
|
+
/**
|
|
174
|
+
* Whether data is currently loading.
|
|
175
|
+
*
|
|
176
|
+
* @return {boolean}
|
|
177
|
+
*/
|
|
178
|
+
isLoading() {
|
|
179
|
+
return this.loading
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* All rows (external or fetched).
|
|
184
|
+
*
|
|
185
|
+
* @return {object[]}
|
|
186
|
+
*/
|
|
187
|
+
allRows() {
|
|
188
|
+
return this.rows || this.fetchedRows
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Rows limited to the configured limit.
|
|
193
|
+
*
|
|
194
|
+
* @return {object[]}
|
|
195
|
+
*/
|
|
196
|
+
displayRows() {
|
|
197
|
+
if (this.limit > 0) {
|
|
198
|
+
return this.allRows.slice(0, this.limit)
|
|
199
|
+
}
|
|
200
|
+
return this.allRows
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Total count of all rows (before limiting).
|
|
205
|
+
*
|
|
206
|
+
* @return {number}
|
|
207
|
+
*/
|
|
208
|
+
totalCount() {
|
|
209
|
+
return this.allRows.length
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Count of displayed rows (after limiting).
|
|
214
|
+
*
|
|
215
|
+
* @return {number}
|
|
216
|
+
*/
|
|
217
|
+
limitedCount() {
|
|
218
|
+
return this.displayRows.length
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
mounted() {
|
|
223
|
+
if (this.rows === null && this.register && this.schemaId) {
|
|
224
|
+
this.fetchData()
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
methods: {
|
|
229
|
+
/**
|
|
230
|
+
* Fetch data from the OpenRegister API.
|
|
231
|
+
*
|
|
232
|
+
* @return {Promise<void>}
|
|
233
|
+
*/
|
|
234
|
+
async fetchData() {
|
|
235
|
+
this.loading = true
|
|
236
|
+
try {
|
|
237
|
+
const url = `/index.php/apps/openregister/api/objects/${this.register}/${this.schemaId}`
|
|
238
|
+
const response = await fetch(url, {
|
|
239
|
+
headers: {
|
|
240
|
+
'Content-Type': 'application/json',
|
|
241
|
+
'OCS-APIREQUEST': 'true',
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
if (response.ok) {
|
|
245
|
+
const data = await response.json()
|
|
246
|
+
this.fetchedRows = data.results || data || []
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error('CnTableWidget: Failed to fetch data', error)
|
|
250
|
+
} finally {
|
|
251
|
+
this.loading = false
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Handle row click events. Navigates if rowClickRoute is configured.
|
|
257
|
+
*
|
|
258
|
+
* @param {object} row - The clicked row data.
|
|
259
|
+
*/
|
|
260
|
+
onRowClick(row) {
|
|
261
|
+
if (this.rowClickRoute) {
|
|
262
|
+
const route = this.rowClickRoute(row)
|
|
263
|
+
if (route) {
|
|
264
|
+
this.$router.push(route)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
</script>
|
|
271
|
+
|
|
272
|
+
<style scoped>
|
|
273
|
+
.cn-table-widget {
|
|
274
|
+
background: var(--color-main-background);
|
|
275
|
+
border: 1px solid var(--color-border);
|
|
276
|
+
border-radius: var(--border-radius-large, 16px);
|
|
277
|
+
overflow: hidden;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.cn-table-widget__header {
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
justify-content: space-between;
|
|
284
|
+
padding: 12px 16px;
|
|
285
|
+
border-bottom: 1px solid var(--color-border);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.cn-table-widget__title {
|
|
289
|
+
margin: 0;
|
|
290
|
+
font-size: 14px;
|
|
291
|
+
font-weight: 600;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.cn-table-widget__count {
|
|
295
|
+
font-size: 12px;
|
|
296
|
+
color: var(--color-text-maxcontrast);
|
|
297
|
+
background: var(--color-background-dark);
|
|
298
|
+
padding: 2px 8px;
|
|
299
|
+
border-radius: 10px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.cn-table-widget__loading {
|
|
303
|
+
padding: 32px 0;
|
|
304
|
+
display: flex;
|
|
305
|
+
justify-content: center;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.cn-table-widget__empty {
|
|
309
|
+
padding: 24px 16px;
|
|
310
|
+
text-align: center;
|
|
311
|
+
color: var(--color-text-maxcontrast);
|
|
312
|
+
font-size: 14px;
|
|
313
|
+
margin: 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.cn-table-widget__footer {
|
|
317
|
+
padding: 8px 16px;
|
|
318
|
+
border-top: 1px solid var(--color-border);
|
|
319
|
+
text-align: center;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.cn-table-widget__view-all {
|
|
323
|
+
font-size: 13px;
|
|
324
|
+
color: var(--color-primary-element);
|
|
325
|
+
cursor: pointer;
|
|
326
|
+
text-decoration: none;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.cn-table-widget__view-all:hover {
|
|
330
|
+
text-decoration: underline;
|
|
331
|
+
}
|
|
332
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CnTableWidget } from './CnTableWidget.vue'
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
CnTasksCard — Inline tasks card for detail pages.
|
|
3
|
+
|
|
4
|
+
Displays up to 5 tasks with status indicators, assignee, and due date.
|
|
5
|
+
Integrates CnUserActionMenu on assignee names. Highlights overdue tasks.
|
|
6
|
+
Wraps CnDetailCard for consistent styling.
|
|
7
|
+
-->
|
|
8
|
+
<template>
|
|
9
|
+
<CnDetailCard :title="titleLabel" :icon="CheckboxMarkedOutline" :collapsible="collapsible">
|
|
10
|
+
<div class="cn-tasks-card">
|
|
11
|
+
<!-- Loading state -->
|
|
12
|
+
<NcLoadingIcon v-if="loading" />
|
|
13
|
+
|
|
14
|
+
<!-- Empty state -->
|
|
15
|
+
<div v-else-if="allTasks.length === 0" class="cn-tasks-card__empty">
|
|
16
|
+
{{ noTasksLabel }}
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<!-- Tasks list -->
|
|
20
|
+
<div v-else class="cn-tasks-card__list">
|
|
21
|
+
<div
|
|
22
|
+
v-for="task in displayedTasks"
|
|
23
|
+
:key="task.id"
|
|
24
|
+
class="cn-tasks-card__task">
|
|
25
|
+
<!-- Status icon -->
|
|
26
|
+
<div class="cn-tasks-card__status-icon">
|
|
27
|
+
<CheckboxMarkedOutline
|
|
28
|
+
v-if="task.status === 'completed'"
|
|
29
|
+
:size="20"
|
|
30
|
+
class="cn-tasks-card__icon--completed" />
|
|
31
|
+
<ProgressClock
|
|
32
|
+
v-else-if="task.status === 'active' || task.status === 'in-process'"
|
|
33
|
+
:size="20"
|
|
34
|
+
class="cn-tasks-card__icon--active" />
|
|
35
|
+
<CloseCircleOutline
|
|
36
|
+
v-else-if="task.status === 'terminated'"
|
|
37
|
+
:size="20"
|
|
38
|
+
class="cn-tasks-card__icon--terminated" />
|
|
39
|
+
<CheckboxBlankOutline
|
|
40
|
+
v-else
|
|
41
|
+
:size="20"
|
|
42
|
+
class="cn-tasks-card__icon--available" />
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Task content -->
|
|
46
|
+
<div class="cn-tasks-card__content">
|
|
47
|
+
<span class="cn-tasks-card__title">{{ task.title || task.name }}</span>
|
|
48
|
+
<div class="cn-tasks-card__meta">
|
|
49
|
+
<!-- Assignee -->
|
|
50
|
+
<span v-if="hasAssignee(task)" class="cn-tasks-card__assignee">
|
|
51
|
+
<CnUserActionMenu
|
|
52
|
+
v-if="!isCurrentUser(task.assignee)"
|
|
53
|
+
:user-id="task.assignee"
|
|
54
|
+
:display-name="task.assignee">
|
|
55
|
+
<span class="cn-tasks-card__assignee-name">{{ task.assignee }}</span>
|
|
56
|
+
</CnUserActionMenu>
|
|
57
|
+
<span v-else class="cn-tasks-card__assignee-name cn-tasks-card__assignee-name--self">
|
|
58
|
+
{{ task.assignee }}
|
|
59
|
+
</span>
|
|
60
|
+
</span>
|
|
61
|
+
<span v-else class="cn-tasks-card__unassigned">
|
|
62
|
+
{{ unassignedLabel }}
|
|
63
|
+
</span>
|
|
64
|
+
|
|
65
|
+
<!-- Due date -->
|
|
66
|
+
<span
|
|
67
|
+
v-if="task.dueDate"
|
|
68
|
+
class="cn-tasks-card__due-date"
|
|
69
|
+
:class="{ 'cn-tasks-card__due-date--overdue': isOverdue(task) }">
|
|
70
|
+
{{ formatDate(task.dueDate) }}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- Footer: "Show all" link -->
|
|
79
|
+
<template v-if="allTasks.length > maxDisplay" #footer>
|
|
80
|
+
<button
|
|
81
|
+
class="cn-tasks-card__show-all"
|
|
82
|
+
@click="$emit('show-all')">
|
|
83
|
+
{{ showAllLabel }} ({{ allTasks.length }})
|
|
84
|
+
</button>
|
|
85
|
+
</template>
|
|
86
|
+
</CnDetailCard>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<script>
|
|
90
|
+
import { NcLoadingIcon } from '@nextcloud/vue'
|
|
91
|
+
import CheckboxMarkedOutline from 'vue-material-design-icons/CheckboxMarkedOutline.vue'
|
|
92
|
+
import CheckboxBlankOutline from 'vue-material-design-icons/CheckboxBlankOutline.vue'
|
|
93
|
+
import ProgressClock from 'vue-material-design-icons/ProgressClock.vue'
|
|
94
|
+
import CloseCircleOutline from 'vue-material-design-icons/CloseCircleOutline.vue'
|
|
95
|
+
|
|
96
|
+
import CnDetailCard from '../CnDetailCard/CnDetailCard.vue'
|
|
97
|
+
import CnUserActionMenu from '../CnUserActionMenu/CnUserActionMenu.vue'
|
|
98
|
+
import { buildHeaders } from '../../utils/index.js'
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* CnTasksCard — Inline tasks widget for detail pages.
|
|
102
|
+
*
|
|
103
|
+
* Shows up to 5 tasks sorted by due date with status indicators.
|
|
104
|
+
* Highlights overdue tasks and integrates CnUserActionMenu on assignees.
|
|
105
|
+
*
|
|
106
|
+
* @example Basic usage
|
|
107
|
+
* <CnTasksCard
|
|
108
|
+
* register-id="uuid-register"
|
|
109
|
+
* schema-id="uuid-schema"
|
|
110
|
+
* object-id="uuid-object" />
|
|
111
|
+
*
|
|
112
|
+
* @example With sidebar sync
|
|
113
|
+
* <CnTasksCard
|
|
114
|
+
* register-id="reg"
|
|
115
|
+
* schema-id="schema"
|
|
116
|
+
* object-id="obj"
|
|
117
|
+
* @show-all="openSidebarTasksTab" />
|
|
118
|
+
*/
|
|
119
|
+
export default {
|
|
120
|
+
name: 'CnTasksCard',
|
|
121
|
+
|
|
122
|
+
components: {
|
|
123
|
+
CnDetailCard,
|
|
124
|
+
CnUserActionMenu,
|
|
125
|
+
NcLoadingIcon,
|
|
126
|
+
CheckboxMarkedOutline,
|
|
127
|
+
CheckboxBlankOutline,
|
|
128
|
+
ProgressClock,
|
|
129
|
+
CloseCircleOutline,
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
props: {
|
|
133
|
+
/** OpenRegister register ID */
|
|
134
|
+
registerId: {
|
|
135
|
+
type: String,
|
|
136
|
+
required: true,
|
|
137
|
+
},
|
|
138
|
+
/** OpenRegister schema ID */
|
|
139
|
+
schemaId: {
|
|
140
|
+
type: String,
|
|
141
|
+
required: true,
|
|
142
|
+
},
|
|
143
|
+
/** Object UUID */
|
|
144
|
+
objectId: {
|
|
145
|
+
type: String,
|
|
146
|
+
required: true,
|
|
147
|
+
},
|
|
148
|
+
/** Base API URL for OpenRegister */
|
|
149
|
+
apiBase: {
|
|
150
|
+
type: String,
|
|
151
|
+
default: '/apps/openregister/api',
|
|
152
|
+
},
|
|
153
|
+
/** Maximum number of tasks to display */
|
|
154
|
+
maxDisplay: {
|
|
155
|
+
type: Number,
|
|
156
|
+
default: 5,
|
|
157
|
+
},
|
|
158
|
+
/** Whether the card is collapsible */
|
|
159
|
+
collapsible: {
|
|
160
|
+
type: Boolean,
|
|
161
|
+
default: false,
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// --- Pre-translated labels ---
|
|
165
|
+
titleLabel: { type: String, default: 'Tasks' },
|
|
166
|
+
noTasksLabel: { type: String, default: 'No tasks' },
|
|
167
|
+
showAllLabel: { type: String, default: 'Show all' },
|
|
168
|
+
unassignedLabel: { type: String, default: 'Unassigned' },
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
emits: ['show-all'],
|
|
172
|
+
|
|
173
|
+
data() {
|
|
174
|
+
return {
|
|
175
|
+
CheckboxMarkedOutline,
|
|
176
|
+
allTasks: [],
|
|
177
|
+
loading: false,
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
computed: {
|
|
182
|
+
displayedTasks() {
|
|
183
|
+
// Sort by due date (soonest first), then limit
|
|
184
|
+
const sorted = [...this.allTasks].sort((a, b) => {
|
|
185
|
+
const dateA = a.dueDate ? new Date(a.dueDate) : new Date('9999-12-31')
|
|
186
|
+
const dateB = b.dueDate ? new Date(b.dueDate) : new Date('9999-12-31')
|
|
187
|
+
return dateA - dateB
|
|
188
|
+
})
|
|
189
|
+
return sorted.slice(0, this.maxDisplay)
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
watch: {
|
|
194
|
+
objectId: {
|
|
195
|
+
immediate: true,
|
|
196
|
+
handler(newId) {
|
|
197
|
+
if (newId && this.registerId && this.schemaId) {
|
|
198
|
+
this.fetchTasks()
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
methods: {
|
|
205
|
+
hasAssignee(task) {
|
|
206
|
+
return task.assignee && task.assignee.trim() !== ''
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
isCurrentUser(userId) {
|
|
210
|
+
const currentUser = typeof OC !== 'undefined' ? OC?.currentUser : null
|
|
211
|
+
return userId === currentUser
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
isOverdue(task) {
|
|
215
|
+
if (!task.dueDate || task.status === 'completed') return false
|
|
216
|
+
try {
|
|
217
|
+
return new Date(task.dueDate) < new Date()
|
|
218
|
+
} catch {
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
async fetchTasks() {
|
|
224
|
+
if (!this.registerId || !this.schemaId || !this.objectId) return
|
|
225
|
+
this.loading = true
|
|
226
|
+
try {
|
|
227
|
+
const url = `${this.apiBase}/objects/${this.registerId}/${this.schemaId}/${this.objectId}/tasks`
|
|
228
|
+
const response = await fetch(url, { headers: buildHeaders() })
|
|
229
|
+
if (response.ok) {
|
|
230
|
+
const data = await response.json()
|
|
231
|
+
this.allTasks = data.results || data || []
|
|
232
|
+
}
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.error('CnTasksCard: Failed to fetch tasks', err)
|
|
235
|
+
} finally {
|
|
236
|
+
this.loading = false
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
formatDate(dateStr) {
|
|
241
|
+
if (!dateStr) return ''
|
|
242
|
+
try {
|
|
243
|
+
return new Date(dateStr).toLocaleDateString(undefined, {
|
|
244
|
+
year: 'numeric',
|
|
245
|
+
month: 'short',
|
|
246
|
+
day: 'numeric',
|
|
247
|
+
})
|
|
248
|
+
} catch {
|
|
249
|
+
return dateStr
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
</script>
|
|
255
|
+
|
|
256
|
+
<style scoped>
|
|
257
|
+
.cn-tasks-card__empty {
|
|
258
|
+
text-align: center;
|
|
259
|
+
padding: 16px 12px;
|
|
260
|
+
color: var(--color-text-maxcontrast);
|
|
261
|
+
font-size: 13px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.cn-tasks-card__list {
|
|
265
|
+
display: flex;
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.cn-tasks-card__task {
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: flex-start;
|
|
272
|
+
gap: 10px;
|
|
273
|
+
padding: 8px 0;
|
|
274
|
+
border-bottom: 1px solid var(--color-border);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.cn-tasks-card__task:last-child {
|
|
278
|
+
border-bottom: none;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.cn-tasks-card__status-icon {
|
|
282
|
+
flex-shrink: 0;
|
|
283
|
+
padding-top: 1px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.cn-tasks-card__icon--completed {
|
|
287
|
+
color: var(--color-success);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.cn-tasks-card__icon--active {
|
|
291
|
+
color: var(--color-primary-element);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.cn-tasks-card__icon--terminated {
|
|
295
|
+
color: var(--color-error);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.cn-tasks-card__icon--available {
|
|
299
|
+
color: var(--color-text-maxcontrast);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.cn-tasks-card__content {
|
|
303
|
+
flex: 1;
|
|
304
|
+
min-width: 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.cn-tasks-card__title {
|
|
308
|
+
display: block;
|
|
309
|
+
font-size: 13px;
|
|
310
|
+
font-weight: 500;
|
|
311
|
+
white-space: nowrap;
|
|
312
|
+
overflow: hidden;
|
|
313
|
+
text-overflow: ellipsis;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.cn-tasks-card__meta {
|
|
317
|
+
display: flex;
|
|
318
|
+
align-items: center;
|
|
319
|
+
gap: 8px;
|
|
320
|
+
margin-top: 2px;
|
|
321
|
+
font-size: 12px;
|
|
322
|
+
color: var(--color-text-maxcontrast);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.cn-tasks-card__assignee-name {
|
|
326
|
+
color: var(--color-primary-element);
|
|
327
|
+
font-weight: 500;
|
|
328
|
+
cursor: pointer;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.cn-tasks-card__assignee-name:hover {
|
|
332
|
+
text-decoration: underline;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.cn-tasks-card__assignee-name--self {
|
|
336
|
+
color: var(--color-text-maxcontrast);
|
|
337
|
+
cursor: default;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.cn-tasks-card__assignee-name--self:hover {
|
|
341
|
+
text-decoration: none;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.cn-tasks-card__unassigned {
|
|
345
|
+
font-style: italic;
|
|
346
|
+
color: var(--color-text-maxcontrast);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.cn-tasks-card__due-date {
|
|
350
|
+
white-space: nowrap;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.cn-tasks-card__due-date--overdue {
|
|
354
|
+
color: var(--color-error);
|
|
355
|
+
font-weight: 500;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.cn-tasks-card__show-all {
|
|
359
|
+
background: none;
|
|
360
|
+
border: none;
|
|
361
|
+
color: var(--color-primary-element);
|
|
362
|
+
font-size: 13px;
|
|
363
|
+
font-weight: 500;
|
|
364
|
+
cursor: pointer;
|
|
365
|
+
padding: 0;
|
|
366
|
+
width: 100%;
|
|
367
|
+
text-align: center;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.cn-tasks-card__show-all:hover {
|
|
371
|
+
text-decoration: underline;
|
|
372
|
+
}
|
|
373
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CnTasksCard } from './CnTasksCard.vue'
|