@conduction/nextcloud-vue 0.1.0-beta.4 → 0.1.0-beta.6
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/nextcloud-vue.cjs +67614 -0
- package/dist/nextcloud-vue.cjs.js +9559 -8983
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.cjs.map +1 -0
- package/dist/nextcloud-vue.css +1231 -1231
- package/dist/nextcloud-vue.esm.js +9559 -8983
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +14 -5
- package/src/components/CnActionsBar/CnActionsBar.vue +235 -235
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -579
- package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -217
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -121
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -418
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -247
- package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
- package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
- package/src/components/CnChartWidget/CnChartWidget.vue +320 -320
- package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
- package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -250
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -225
- package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -390
- package/src/components/CnDataTable/CnDataTable.vue +349 -349
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -170
- package/src/components/CnDetailCard/CnDetailCard.vue +214 -214
- package/src/components/CnDetailPage/CnDetailPage.vue +285 -281
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -231
- package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
- package/src/components/CnFormDialog/CnFormDialog.vue +302 -11
- package/src/components/CnIcon/CnIcon.vue +89 -89
- package/src/components/CnIndexPage/CnIndexPage.vue +884 -874
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -503
- package/src/components/CnItemCard/CnItemCard.vue +132 -132
- package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
- package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
- package/src/components/CnNoteCard/CnNoteCard.vue +149 -149
- package/src/components/CnNotesCard/CnNotesCard.vue +413 -413
- package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
- package/src/components/CnObjectCard/eslint-setup.md +235 -0
- package/src/components/CnObjectCard/package.json-or.json +132 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -876
- package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
- package/src/components/CnPagination/CnPagination.vue +252 -252
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -792
- package/src/components/CnRowActions/CnRowActions.vue +95 -73
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -226
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -787
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -305
- package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -1398
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -236
- package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
- package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
- package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -420
- package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -540
- package/src/components/CnTasksCard/CnTasksCard.vue +373 -373
- package/src/components/CnTileWidget/CnTileWidget.vue +159 -159
- package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -292
- package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -435
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
- package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -180
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -211
- package/src/index.js +1 -1
- package/src/types/notification.d.ts +13 -13
- package/src/types/organisation.d.ts +15 -15
- package/src/types/schema.d.ts +13 -13
- package/src/types/task.d.ts +6 -6
- package/src/utils/headers.js +5 -3
|
@@ -1,373 +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>
|
|
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>
|