@flusys/ng-task-manager 5.3.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/README.md +276 -0
- package/fesm2022/flusys-ng-task-manager.mjs +3771 -0
- package/fesm2022/flusys-ng-task-manager.mjs.map +1 -0
- package/package.json +35 -0
- package/types/flusys-ng-task-manager.d.ts +580 -0
|
@@ -0,0 +1,3771 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Injectable, inject, signal, DestroyRef, computed, input, output, Component, model, effect, untracked } from '@angular/core';
|
|
3
|
+
import { ApiResourceService, TranslatePipe, AngularModule, PrimeModule, TranslateService, UserSelectComponent, resolveTranslationModule, SHARED_MESSAGES } from '@flusys/ng-shared';
|
|
4
|
+
import { firstValueFrom, Subject } from 'rxjs';
|
|
5
|
+
import { io } from 'socket.io-client';
|
|
6
|
+
import { APP_CONFIG } from '@flusys/ng-core';
|
|
7
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
8
|
+
import * as i9$1 from '@angular/common';
|
|
9
|
+
import { DatePipe } from '@angular/common';
|
|
10
|
+
import * as i1 from 'primeng/tag';
|
|
11
|
+
import { TagModule } from 'primeng/tag';
|
|
12
|
+
import * as i2 from 'primeng/tooltip';
|
|
13
|
+
import { TooltipModule } from 'primeng/tooltip';
|
|
14
|
+
import * as i3 from '@angular/forms';
|
|
15
|
+
import { FormsModule } from '@angular/forms';
|
|
16
|
+
import * as i9 from 'primeng/button';
|
|
17
|
+
import { ButtonModule } from 'primeng/button';
|
|
18
|
+
import * as i7 from 'primeng/datepicker';
|
|
19
|
+
import { DatePickerModule } from 'primeng/datepicker';
|
|
20
|
+
import * as i1$1 from 'primeng/dialog';
|
|
21
|
+
import { DialogModule } from 'primeng/dialog';
|
|
22
|
+
import * as i4 from 'primeng/inputtext';
|
|
23
|
+
import { InputTextModule } from 'primeng/inputtext';
|
|
24
|
+
import * as i8 from 'primeng/multiselect';
|
|
25
|
+
import { MultiSelectModule } from 'primeng/multiselect';
|
|
26
|
+
import * as i5 from 'primeng/textarea';
|
|
27
|
+
import { TextareaModule } from 'primeng/textarea';
|
|
28
|
+
import * as i6 from 'primeng/select';
|
|
29
|
+
import { SelectModule } from 'primeng/select';
|
|
30
|
+
import * as i2$1 from 'primeng/api';
|
|
31
|
+
import { ConfirmationService, MessageService } from 'primeng/api';
|
|
32
|
+
import * as i1$2 from 'primeng/drawer';
|
|
33
|
+
import { DrawerModule } from 'primeng/drawer';
|
|
34
|
+
import { Router } from '@angular/router';
|
|
35
|
+
import * as i6$1 from 'primeng/skeleton';
|
|
36
|
+
import * as i1$3 from '@angular/cdk/drag-drop';
|
|
37
|
+
import { moveItemInArray, transferArrayItem, DragDropModule } from '@angular/cdk/drag-drop';
|
|
38
|
+
import * as i9$2 from 'primeng/table';
|
|
39
|
+
|
|
40
|
+
class TaskBoardApiService extends ApiResourceService {
|
|
41
|
+
constructor() {
|
|
42
|
+
super('board', 'taskManager');
|
|
43
|
+
}
|
|
44
|
+
getMembers(boardId) {
|
|
45
|
+
return firstValueFrom(this.http.post(`${this.baseUrl}/get-members`, { boardId }));
|
|
46
|
+
}
|
|
47
|
+
addMember(boardId, userId, role = 'MEMBER') {
|
|
48
|
+
return firstValueFrom(this.http.post(`${this.baseUrl}/add-member`, { boardId, userId, role }));
|
|
49
|
+
}
|
|
50
|
+
removeMember(boardId, userId) {
|
|
51
|
+
return firstValueFrom(this.http.post(`${this.baseUrl}/remove-member`, {
|
|
52
|
+
boardId,
|
|
53
|
+
userId,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
57
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardApiService, providedIn: 'root' });
|
|
58
|
+
}
|
|
59
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardApiService, decorators: [{
|
|
60
|
+
type: Injectable,
|
|
61
|
+
args: [{ providedIn: 'root' }]
|
|
62
|
+
}], ctorParameters: () => [] });
|
|
63
|
+
|
|
64
|
+
class TaskApiService extends ApiResourceService {
|
|
65
|
+
constructor() {
|
|
66
|
+
super('task', 'taskManager');
|
|
67
|
+
}
|
|
68
|
+
moveTask(dto) {
|
|
69
|
+
return firstValueFrom(this.http.post(`${this.baseUrl}/move`, dto));
|
|
70
|
+
}
|
|
71
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
72
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskApiService, providedIn: 'root' });
|
|
73
|
+
}
|
|
74
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskApiService, decorators: [{
|
|
75
|
+
type: Injectable,
|
|
76
|
+
args: [{ providedIn: 'root' }]
|
|
77
|
+
}], ctorParameters: () => [] });
|
|
78
|
+
class TaskListApiService extends ApiResourceService {
|
|
79
|
+
constructor() {
|
|
80
|
+
super('list', 'taskManager');
|
|
81
|
+
}
|
|
82
|
+
updateList(dto) {
|
|
83
|
+
return firstValueFrom(this.http.post(`${this.baseUrl}/update`, dto));
|
|
84
|
+
}
|
|
85
|
+
reorderLists(dto) {
|
|
86
|
+
return firstValueFrom(this.http.post(`${this.baseUrl}/reorder`, dto));
|
|
87
|
+
}
|
|
88
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskListApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
89
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskListApiService, providedIn: 'root' });
|
|
90
|
+
}
|
|
91
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskListApiService, decorators: [{
|
|
92
|
+
type: Injectable,
|
|
93
|
+
args: [{ providedIn: 'root' }]
|
|
94
|
+
}], ctorParameters: () => [] });
|
|
95
|
+
class TaskLabelApiService extends ApiResourceService {
|
|
96
|
+
constructor() {
|
|
97
|
+
super('label', 'taskManager');
|
|
98
|
+
}
|
|
99
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskLabelApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
100
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskLabelApiService, providedIn: 'root' });
|
|
101
|
+
}
|
|
102
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskLabelApiService, decorators: [{
|
|
103
|
+
type: Injectable,
|
|
104
|
+
args: [{ providedIn: 'root' }]
|
|
105
|
+
}], ctorParameters: () => [] });
|
|
106
|
+
class TaskCommentApiService extends ApiResourceService {
|
|
107
|
+
constructor() {
|
|
108
|
+
super('comment', 'taskManager');
|
|
109
|
+
}
|
|
110
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskCommentApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
111
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskCommentApiService, providedIn: 'root' });
|
|
112
|
+
}
|
|
113
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskCommentApiService, decorators: [{
|
|
114
|
+
type: Injectable,
|
|
115
|
+
args: [{ providedIn: 'root' }]
|
|
116
|
+
}], ctorParameters: () => [] });
|
|
117
|
+
|
|
118
|
+
class TaskBoardSocketService {
|
|
119
|
+
socket = null;
|
|
120
|
+
appConfig = inject(APP_CONFIG);
|
|
121
|
+
_isConnected = signal(false, ...(ngDevMode ? [{ debugName: "_isConnected" }] : []));
|
|
122
|
+
isConnected = this._isConnected.asReadonly();
|
|
123
|
+
boardEvent$ = new Subject();
|
|
124
|
+
onBoardEvent = this.boardEvent$.asObservable();
|
|
125
|
+
connect(accessToken) {
|
|
126
|
+
if (this.socket?.connected)
|
|
127
|
+
return;
|
|
128
|
+
const baseUrl = this.getBaseUrl();
|
|
129
|
+
this.socket = io(`${baseUrl}/task-boards`, {
|
|
130
|
+
auth: { token: accessToken },
|
|
131
|
+
transports: ['websocket', 'polling'],
|
|
132
|
+
reconnection: true,
|
|
133
|
+
reconnectionAttempts: 5,
|
|
134
|
+
reconnectionDelay: 1000,
|
|
135
|
+
});
|
|
136
|
+
this.socket.on('connect', () => this._isConnected.set(true));
|
|
137
|
+
this.socket.on('disconnect', (reason) => {
|
|
138
|
+
this._isConnected.set(false);
|
|
139
|
+
if (reason === 'io server disconnect')
|
|
140
|
+
this.socket?.connect();
|
|
141
|
+
});
|
|
142
|
+
this.socket.on('connect_error', () => this._isConnected.set(false));
|
|
143
|
+
this.socket.on('board:event', (event) => this.boardEvent$.next(event));
|
|
144
|
+
}
|
|
145
|
+
joinBoard(boardId) {
|
|
146
|
+
this.socket?.emit('board:join', { boardId });
|
|
147
|
+
}
|
|
148
|
+
leaveBoard(boardId) {
|
|
149
|
+
this.socket?.emit('board:leave', { boardId });
|
|
150
|
+
}
|
|
151
|
+
disconnect() {
|
|
152
|
+
this.socket?.disconnect();
|
|
153
|
+
this.socket = null;
|
|
154
|
+
this._isConnected.set(false);
|
|
155
|
+
}
|
|
156
|
+
getBaseUrl() {
|
|
157
|
+
const services = this.appConfig.services;
|
|
158
|
+
return services?.['taskManager']?.socketUrl ?? this.appConfig.apiBaseUrl;
|
|
159
|
+
}
|
|
160
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardSocketService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
161
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardSocketService, providedIn: 'root' });
|
|
162
|
+
}
|
|
163
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardSocketService, decorators: [{
|
|
164
|
+
type: Injectable,
|
|
165
|
+
args: [{ providedIn: 'root' }]
|
|
166
|
+
}] });
|
|
167
|
+
|
|
168
|
+
class TaskBoardStateService {
|
|
169
|
+
boardApi = inject(TaskBoardApiService);
|
|
170
|
+
taskApi = inject(TaskApiService);
|
|
171
|
+
listApi = inject(TaskListApiService);
|
|
172
|
+
labelApi = inject(TaskLabelApiService);
|
|
173
|
+
socketService = inject(TaskBoardSocketService);
|
|
174
|
+
destroyRef = inject(DestroyRef);
|
|
175
|
+
_boards = signal([], ...(ngDevMode ? [{ debugName: "_boards" }] : []));
|
|
176
|
+
_activeBoard = signal(null, ...(ngDevMode ? [{ debugName: "_activeBoard" }] : []));
|
|
177
|
+
_lists = signal([], ...(ngDevMode ? [{ debugName: "_lists" }] : []));
|
|
178
|
+
_tasks = signal([], ...(ngDevMode ? [{ debugName: "_tasks" }] : []));
|
|
179
|
+
_labels = signal([], ...(ngDevMode ? [{ debugName: "_labels" }] : []));
|
|
180
|
+
_members = signal([], ...(ngDevMode ? [{ debugName: "_members" }] : []));
|
|
181
|
+
_selectedTask = signal(null, ...(ngDevMode ? [{ debugName: "_selectedTask" }] : []));
|
|
182
|
+
_isLoading = signal(false, ...(ngDevMode ? [{ debugName: "_isLoading" }] : []));
|
|
183
|
+
_isBoardLoading = signal(false, ...(ngDevMode ? [{ debugName: "_isBoardLoading" }] : []));
|
|
184
|
+
boards = this._boards.asReadonly();
|
|
185
|
+
activeBoard = this._activeBoard.asReadonly();
|
|
186
|
+
lists = this._lists.asReadonly();
|
|
187
|
+
tasks = this._tasks.asReadonly();
|
|
188
|
+
labels = this._labels.asReadonly();
|
|
189
|
+
members = this._members.asReadonly();
|
|
190
|
+
selectedTask = this._selectedTask.asReadonly();
|
|
191
|
+
isLoading = this._isLoading.asReadonly();
|
|
192
|
+
isBoardLoading = this._isBoardLoading.asReadonly();
|
|
193
|
+
tasksByList = computed(() => {
|
|
194
|
+
const map = new Map();
|
|
195
|
+
for (const task of this._tasks()) {
|
|
196
|
+
const list = map.get(task.listId) ?? [];
|
|
197
|
+
list.push(task);
|
|
198
|
+
map.set(task.listId, list);
|
|
199
|
+
}
|
|
200
|
+
return map;
|
|
201
|
+
}, ...(ngDevMode ? [{ debugName: "tasksByList" }] : []));
|
|
202
|
+
listsWithTasks = computed(() => this._lists()
|
|
203
|
+
.slice()
|
|
204
|
+
.sort((a, b) => a.position - b.position)
|
|
205
|
+
.map((list) => ({
|
|
206
|
+
...list,
|
|
207
|
+
tasks: (this.tasksByList().get(list.id) ?? []).slice().sort((a, b) => a.position - b.position),
|
|
208
|
+
})), ...(ngDevMode ? [{ debugName: "listsWithTasks" }] : []));
|
|
209
|
+
hasActiveBoard = computed(() => this._activeBoard() !== null, ...(ngDevMode ? [{ debugName: "hasActiveBoard" }] : []));
|
|
210
|
+
isSocketConnected = this.socketService.isConnected;
|
|
211
|
+
constructor() {
|
|
212
|
+
this.socketService.onBoardEvent
|
|
213
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
214
|
+
.subscribe((event) => this.handleBoardEvent(event));
|
|
215
|
+
}
|
|
216
|
+
async loadBoards() {
|
|
217
|
+
this._isLoading.set(true);
|
|
218
|
+
try {
|
|
219
|
+
const res = await this.boardApi.getAll({ sort: { createdAt: 'DESC' } });
|
|
220
|
+
this._boards.set(res.data ?? []);
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
this._isLoading.set(false);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async openBoard(boardId) {
|
|
227
|
+
this._isBoardLoading.set(true);
|
|
228
|
+
try {
|
|
229
|
+
const prevBoard = this._activeBoard();
|
|
230
|
+
if (prevBoard)
|
|
231
|
+
this.socketService.leaveBoard(prevBoard.id);
|
|
232
|
+
const [boardRes, listsRes, tasksRes, labelsRes, membersRes] = await Promise.all([
|
|
233
|
+
this.boardApi.findById(boardId),
|
|
234
|
+
this.listApi.getAll({ filter: { boardId, isActive: true }, sort: { position: 'ASC' } }),
|
|
235
|
+
this.taskApi.getAll({ filter: { boardId }, sort: { position: 'ASC' } }),
|
|
236
|
+
this.labelApi.getAll({ filter: { boardId } }),
|
|
237
|
+
this.boardApi.getMembers(boardId),
|
|
238
|
+
]);
|
|
239
|
+
this._activeBoard.set(boardRes.data ?? null);
|
|
240
|
+
this._lists.set(listsRes.data ?? []);
|
|
241
|
+
this._tasks.set(tasksRes.data ?? []);
|
|
242
|
+
this._labels.set(labelsRes.data ?? []);
|
|
243
|
+
this._members.set(membersRes.data ?? []);
|
|
244
|
+
this.socketService.joinBoard(boardId);
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
this._isBoardLoading.set(false);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
closeBoard() {
|
|
251
|
+
const board = this._activeBoard();
|
|
252
|
+
if (board)
|
|
253
|
+
this.socketService.leaveBoard(board.id);
|
|
254
|
+
this._activeBoard.set(null);
|
|
255
|
+
this._lists.set([]);
|
|
256
|
+
this._tasks.set([]);
|
|
257
|
+
this._labels.set([]);
|
|
258
|
+
this._members.set([]);
|
|
259
|
+
this._selectedTask.set(null);
|
|
260
|
+
}
|
|
261
|
+
selectTask(task) {
|
|
262
|
+
this._selectedTask.set(task);
|
|
263
|
+
}
|
|
264
|
+
async addLabel(boardId, name, color) {
|
|
265
|
+
await this.labelApi.insert({ boardId, name, color });
|
|
266
|
+
const res = await this.labelApi.getAll({ filter: { boardId } });
|
|
267
|
+
this._labels.set(res.data ?? []);
|
|
268
|
+
}
|
|
269
|
+
async deleteLabel(labelId) {
|
|
270
|
+
await this.labelApi.delete({ id: labelId, type: 'delete' });
|
|
271
|
+
this._labels.update((list) => list.filter((l) => l.id !== labelId));
|
|
272
|
+
}
|
|
273
|
+
async addMember(userId, role = 'MEMBER') {
|
|
274
|
+
const board = this._activeBoard();
|
|
275
|
+
if (!board)
|
|
276
|
+
return;
|
|
277
|
+
const res = await this.boardApi.addMember(board.id, userId, role);
|
|
278
|
+
if (res.data)
|
|
279
|
+
this._members.update((list) => [...list, res.data]);
|
|
280
|
+
}
|
|
281
|
+
async removeMember(userId) {
|
|
282
|
+
const board = this._activeBoard();
|
|
283
|
+
if (!board)
|
|
284
|
+
return;
|
|
285
|
+
await this.boardApi.removeMember(board.id, userId);
|
|
286
|
+
this._members.update((list) => list.filter((m) => m.userId !== userId));
|
|
287
|
+
}
|
|
288
|
+
async reorderLists(boardId, orderedIds) {
|
|
289
|
+
const posMap = new Map(orderedIds.map((id, i) => [id, i * 100]));
|
|
290
|
+
this._lists.update((list) => list
|
|
291
|
+
.map((l) => (posMap.has(l.id) ? { ...l, position: posMap.get(l.id) } : l))
|
|
292
|
+
.sort((a, b) => a.position - b.position));
|
|
293
|
+
await this.listApi.reorderLists({ boardId, orderedIds });
|
|
294
|
+
}
|
|
295
|
+
async renameList(listId, name) {
|
|
296
|
+
await this.listApi.updateList({ id: listId, name });
|
|
297
|
+
this._lists.update((list) => list.map((l) => (l.id === listId ? { ...l, name } : l)));
|
|
298
|
+
}
|
|
299
|
+
async updateBoard(dto) {
|
|
300
|
+
await this.boardApi.update(dto);
|
|
301
|
+
const id = dto.id;
|
|
302
|
+
if (!id)
|
|
303
|
+
return;
|
|
304
|
+
this._activeBoard.update((b) => (b?.id === id ? { ...b, ...dto } : b));
|
|
305
|
+
this._boards.update((list) => list.map((b) => (b.id === id ? { ...b, ...dto } : b)));
|
|
306
|
+
}
|
|
307
|
+
addTaskOptimistic(task) {
|
|
308
|
+
this._tasks.update((list) => {
|
|
309
|
+
if (list.some((t) => t.id === task.id)) {
|
|
310
|
+
return list.map((t) => (t.id === task.id ? { ...t, ...task } : t));
|
|
311
|
+
}
|
|
312
|
+
return [...list, task];
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async deleteTask(taskId) {
|
|
316
|
+
await this.taskApi.delete({ id: taskId, type: 'delete' });
|
|
317
|
+
this._tasks.update((list) => list.filter((t) => t.id !== taskId));
|
|
318
|
+
if (this._selectedTask()?.id === taskId)
|
|
319
|
+
this._selectedTask.set(null);
|
|
320
|
+
}
|
|
321
|
+
handleBoardEvent(event) {
|
|
322
|
+
switch (event.type) {
|
|
323
|
+
case 'task:created': {
|
|
324
|
+
const created = event.payload;
|
|
325
|
+
this._tasks.update((list) => {
|
|
326
|
+
if (list.some((t) => t.id === created.id))
|
|
327
|
+
return list;
|
|
328
|
+
return [...list, created];
|
|
329
|
+
});
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case 'task:updated':
|
|
333
|
+
case 'task:moved': {
|
|
334
|
+
const updated = event.payload;
|
|
335
|
+
this._tasks.update((list) => list.map((t) => (t.id === updated.id ? { ...t, ...updated } : t)));
|
|
336
|
+
if (this._selectedTask()?.id === updated.id) {
|
|
337
|
+
this._selectedTask.update((t) => (t ? { ...t, ...updated } : null));
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case 'task:deleted': {
|
|
342
|
+
const deleted = event.payload;
|
|
343
|
+
this._tasks.update((list) => list.filter((t) => t.id !== deleted.id));
|
|
344
|
+
if (this._selectedTask()?.id === deleted.id)
|
|
345
|
+
this._selectedTask.set(null);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case 'list:created':
|
|
349
|
+
this._lists.update((list) => [...list, event.payload]);
|
|
350
|
+
break;
|
|
351
|
+
case 'list:updated': {
|
|
352
|
+
const updatedList = event.payload;
|
|
353
|
+
this._lists.update((list) => list.map((l) => (l.id === updatedList.id ? { ...l, ...updatedList } : l)));
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case 'list:deleted': {
|
|
357
|
+
const deletedList = event.payload;
|
|
358
|
+
this._lists.update((list) => list.filter((l) => l.id !== deletedList.id));
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case 'list:reordered': {
|
|
362
|
+
const { orderedIds } = event.payload;
|
|
363
|
+
const posMap = new Map(orderedIds.map((id, i) => [id, i * 100]));
|
|
364
|
+
this._lists.update((list) => list
|
|
365
|
+
.map((l) => (posMap.has(l.id) ? { ...l, position: posMap.get(l.id) } : l))
|
|
366
|
+
.sort((a, b) => a.position - b.position));
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
372
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardStateService, providedIn: 'root' });
|
|
373
|
+
}
|
|
374
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardStateService, decorators: [{
|
|
375
|
+
type: Injectable,
|
|
376
|
+
args: [{ providedIn: 'root' }]
|
|
377
|
+
}], ctorParameters: () => [] });
|
|
378
|
+
|
|
379
|
+
const MEMBER_COLOR_PALETTE = [
|
|
380
|
+
'#6366f1',
|
|
381
|
+
'#8b5cf6',
|
|
382
|
+
'#ec4899',
|
|
383
|
+
'#ef4444',
|
|
384
|
+
'#f97316',
|
|
385
|
+
'#eab308',
|
|
386
|
+
'#22c55e',
|
|
387
|
+
'#14b8a6',
|
|
388
|
+
'#3b82f6',
|
|
389
|
+
'#06b6d4',
|
|
390
|
+
];
|
|
391
|
+
const BOARD_COLOR_PALETTE = [
|
|
392
|
+
...MEMBER_COLOR_PALETTE,
|
|
393
|
+
'#64748b',
|
|
394
|
+
];
|
|
395
|
+
function taskPrioritySeverity(priority) {
|
|
396
|
+
const map = {
|
|
397
|
+
LOW: 'secondary',
|
|
398
|
+
MEDIUM: 'info',
|
|
399
|
+
HIGH: 'warn',
|
|
400
|
+
URGENT: 'danger',
|
|
401
|
+
};
|
|
402
|
+
return map[priority] ?? 'info';
|
|
403
|
+
}
|
|
404
|
+
function memberInitials(name) {
|
|
405
|
+
if (!name)
|
|
406
|
+
return '?';
|
|
407
|
+
return name
|
|
408
|
+
.split(' ')
|
|
409
|
+
.slice(0, 2)
|
|
410
|
+
.map((n) => n[0]?.toUpperCase() ?? '')
|
|
411
|
+
.join('');
|
|
412
|
+
}
|
|
413
|
+
function isTaskOverdue(dueDate) {
|
|
414
|
+
return new Date(dueDate) < new Date();
|
|
415
|
+
}
|
|
416
|
+
function memberColorFromId(userId) {
|
|
417
|
+
let hash = 0;
|
|
418
|
+
for (let i = 0; i < userId.length; i++)
|
|
419
|
+
hash = userId.charCodeAt(i) + ((hash << 5) - hash);
|
|
420
|
+
return MEMBER_COLOR_PALETTE[Math.abs(hash) % MEMBER_COLOR_PALETTE.length];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
class TaskCardComponent {
|
|
424
|
+
task = input.required(...(ngDevMode ? [{ debugName: "task" }] : []));
|
|
425
|
+
cardClick = output();
|
|
426
|
+
editClick = output();
|
|
427
|
+
deleteClick = output();
|
|
428
|
+
MAX_VISIBLE = 3;
|
|
429
|
+
visibleAssignees = computed(() => (this.task().assignees ?? []).slice(0, this.MAX_VISIBLE), ...(ngDevMode ? [{ debugName: "visibleAssignees" }] : []));
|
|
430
|
+
overflowCount = computed(() => Math.max(0, (this.task().assignees ?? []).length - this.MAX_VISIBLE), ...(ngDevMode ? [{ debugName: "overflowCount" }] : []));
|
|
431
|
+
prioritySeverity = taskPrioritySeverity;
|
|
432
|
+
initials = memberInitials;
|
|
433
|
+
isOverdue = isTaskOverdue;
|
|
434
|
+
memberColor = memberColorFromId;
|
|
435
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
436
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: TaskCardComponent, isStandalone: true, selector: "tm-task-card", inputs: { task: { classPropertyName: "task", publicName: "task", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { cardClick: "cardClick", editClick: "editClick", deleteClick: "deleteClick" }, ngImport: i0, template: `
|
|
437
|
+
<div
|
|
438
|
+
class="task-card group relative rounded-lg border border-surface-200 dark:border-surface-700 p-3 cursor-pointer shadow-sm hover:shadow-md transition-shadow"
|
|
439
|
+
(click)="cardClick.emit(task())"
|
|
440
|
+
>
|
|
441
|
+
<div class="absolute top-2 right-2 hidden group-hover:flex gap-0.5">
|
|
442
|
+
<button
|
|
443
|
+
class="w-6 h-6 flex items-center justify-center rounded text-surface-400 hover:text-primary-500 hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors"
|
|
444
|
+
[pTooltip]="'shared.edit' | translate"
|
|
445
|
+
tooltipPosition="top"
|
|
446
|
+
(click)="$event.stopPropagation(); editClick.emit(task())"
|
|
447
|
+
>
|
|
448
|
+
<i class="pi pi-pencil text-xs"></i>
|
|
449
|
+
</button>
|
|
450
|
+
<button
|
|
451
|
+
class="w-6 h-6 flex items-center justify-center rounded text-surface-400 hover:text-red-500 hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors"
|
|
452
|
+
[pTooltip]="'shared.delete' | translate"
|
|
453
|
+
tooltipPosition="top"
|
|
454
|
+
(click)="$event.stopPropagation(); deleteClick.emit(task())"
|
|
455
|
+
>
|
|
456
|
+
<i class="pi pi-trash text-xs"></i>
|
|
457
|
+
</button>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
@if (task().labels && task().labels!.length > 0) {
|
|
461
|
+
<div class="flex flex-wrap gap-1 mb-2">
|
|
462
|
+
@for (label of task().labels; track label.id) {
|
|
463
|
+
<span
|
|
464
|
+
class="inline-block h-1.5 rounded-full w-8"
|
|
465
|
+
[style.background-color]="label.color"
|
|
466
|
+
[pTooltip]="label.name"
|
|
467
|
+
></span>
|
|
468
|
+
}
|
|
469
|
+
</div>
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
<p class="text-sm font-semibold line-clamp-2 leading-snug pr-14">
|
|
473
|
+
{{ task().title }}
|
|
474
|
+
</p>
|
|
475
|
+
|
|
476
|
+
<div class="flex items-center justify-between gap-2 mt-auto pt-2">
|
|
477
|
+
<p-tag
|
|
478
|
+
[value]="task().priority"
|
|
479
|
+
[severity]="prioritySeverity(task().priority)"
|
|
480
|
+
styleClass="text-xs"
|
|
481
|
+
/>
|
|
482
|
+
|
|
483
|
+
@if (task().dueDate) {
|
|
484
|
+
<span
|
|
485
|
+
class="text-xs text-surface-500"
|
|
486
|
+
[class.text-red-500]="isOverdue(task().dueDate!)"
|
|
487
|
+
>
|
|
488
|
+
{{ task().dueDate | date: 'MMM d' }}
|
|
489
|
+
</span>
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
@if (visibleAssignees().length > 0) {
|
|
493
|
+
<div class="flex items-center -space-x-1.5">
|
|
494
|
+
@for (a of visibleAssignees(); track a.userId) {
|
|
495
|
+
<span
|
|
496
|
+
class="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white flex-shrink-0 ring-2 ring-white dark:ring-surface-800"
|
|
497
|
+
[style.background-color]="memberColor(a.userId)"
|
|
498
|
+
[pTooltip]="a.userName ?? undefined"
|
|
499
|
+
>
|
|
500
|
+
{{ initials(a.userName) }}
|
|
501
|
+
</span>
|
|
502
|
+
}
|
|
503
|
+
@if (overflowCount() > 0) {
|
|
504
|
+
<span
|
|
505
|
+
class="w-6 h-6 rounded-full bg-surface-200 dark:bg-surface-700 text-surface-600 dark:text-surface-300 flex items-center justify-center text-[10px] font-bold flex-shrink-0 ring-2 ring-white dark:ring-surface-800"
|
|
506
|
+
>
|
|
507
|
+
+{{ overflowCount() }}
|
|
508
|
+
</span>
|
|
509
|
+
}
|
|
510
|
+
</div>
|
|
511
|
+
}
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: TagModule }, { kind: "component", type: i1.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i2.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "pipe", type: DatePipe, name: "date" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
|
|
515
|
+
}
|
|
516
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskCardComponent, decorators: [{
|
|
517
|
+
type: Component,
|
|
518
|
+
args: [{
|
|
519
|
+
selector: 'tm-task-card',
|
|
520
|
+
imports: [TagModule, TooltipModule, DatePipe, TranslatePipe],
|
|
521
|
+
template: `
|
|
522
|
+
<div
|
|
523
|
+
class="task-card group relative rounded-lg border border-surface-200 dark:border-surface-700 p-3 cursor-pointer shadow-sm hover:shadow-md transition-shadow"
|
|
524
|
+
(click)="cardClick.emit(task())"
|
|
525
|
+
>
|
|
526
|
+
<div class="absolute top-2 right-2 hidden group-hover:flex gap-0.5">
|
|
527
|
+
<button
|
|
528
|
+
class="w-6 h-6 flex items-center justify-center rounded text-surface-400 hover:text-primary-500 hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors"
|
|
529
|
+
[pTooltip]="'shared.edit' | translate"
|
|
530
|
+
tooltipPosition="top"
|
|
531
|
+
(click)="$event.stopPropagation(); editClick.emit(task())"
|
|
532
|
+
>
|
|
533
|
+
<i class="pi pi-pencil text-xs"></i>
|
|
534
|
+
</button>
|
|
535
|
+
<button
|
|
536
|
+
class="w-6 h-6 flex items-center justify-center rounded text-surface-400 hover:text-red-500 hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors"
|
|
537
|
+
[pTooltip]="'shared.delete' | translate"
|
|
538
|
+
tooltipPosition="top"
|
|
539
|
+
(click)="$event.stopPropagation(); deleteClick.emit(task())"
|
|
540
|
+
>
|
|
541
|
+
<i class="pi pi-trash text-xs"></i>
|
|
542
|
+
</button>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
@if (task().labels && task().labels!.length > 0) {
|
|
546
|
+
<div class="flex flex-wrap gap-1 mb-2">
|
|
547
|
+
@for (label of task().labels; track label.id) {
|
|
548
|
+
<span
|
|
549
|
+
class="inline-block h-1.5 rounded-full w-8"
|
|
550
|
+
[style.background-color]="label.color"
|
|
551
|
+
[pTooltip]="label.name"
|
|
552
|
+
></span>
|
|
553
|
+
}
|
|
554
|
+
</div>
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
<p class="text-sm font-semibold line-clamp-2 leading-snug pr-14">
|
|
558
|
+
{{ task().title }}
|
|
559
|
+
</p>
|
|
560
|
+
|
|
561
|
+
<div class="flex items-center justify-between gap-2 mt-auto pt-2">
|
|
562
|
+
<p-tag
|
|
563
|
+
[value]="task().priority"
|
|
564
|
+
[severity]="prioritySeverity(task().priority)"
|
|
565
|
+
styleClass="text-xs"
|
|
566
|
+
/>
|
|
567
|
+
|
|
568
|
+
@if (task().dueDate) {
|
|
569
|
+
<span
|
|
570
|
+
class="text-xs text-surface-500"
|
|
571
|
+
[class.text-red-500]="isOverdue(task().dueDate!)"
|
|
572
|
+
>
|
|
573
|
+
{{ task().dueDate | date: 'MMM d' }}
|
|
574
|
+
</span>
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
@if (visibleAssignees().length > 0) {
|
|
578
|
+
<div class="flex items-center -space-x-1.5">
|
|
579
|
+
@for (a of visibleAssignees(); track a.userId) {
|
|
580
|
+
<span
|
|
581
|
+
class="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white flex-shrink-0 ring-2 ring-white dark:ring-surface-800"
|
|
582
|
+
[style.background-color]="memberColor(a.userId)"
|
|
583
|
+
[pTooltip]="a.userName ?? undefined"
|
|
584
|
+
>
|
|
585
|
+
{{ initials(a.userName) }}
|
|
586
|
+
</span>
|
|
587
|
+
}
|
|
588
|
+
@if (overflowCount() > 0) {
|
|
589
|
+
<span
|
|
590
|
+
class="w-6 h-6 rounded-full bg-surface-200 dark:bg-surface-700 text-surface-600 dark:text-surface-300 flex items-center justify-center text-[10px] font-bold flex-shrink-0 ring-2 ring-white dark:ring-surface-800"
|
|
591
|
+
>
|
|
592
|
+
+{{ overflowCount() }}
|
|
593
|
+
</span>
|
|
594
|
+
}
|
|
595
|
+
</div>
|
|
596
|
+
}
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
`,
|
|
600
|
+
}]
|
|
601
|
+
}], propDecorators: { task: [{ type: i0.Input, args: [{ isSignal: true, alias: "task", required: true }] }], cardClick: [{ type: i0.Output, args: ["cardClick"] }], editClick: [{ type: i0.Output, args: ["editClick"] }], deleteClick: [{ type: i0.Output, args: ["deleteClick"] }] } });
|
|
602
|
+
|
|
603
|
+
const PRIORITY_OPTIONS = [
|
|
604
|
+
{ label: 'task.priority.LOW', value: 'LOW' },
|
|
605
|
+
{ label: 'task.priority.MEDIUM', value: 'MEDIUM' },
|
|
606
|
+
{ label: 'task.priority.HIGH', value: 'HIGH' },
|
|
607
|
+
{ label: 'task.priority.URGENT', value: 'URGENT' },
|
|
608
|
+
];
|
|
609
|
+
class TaskFormDialogComponent {
|
|
610
|
+
taskApi = inject(TaskApiService);
|
|
611
|
+
visible = model(false, ...(ngDevMode ? [{ debugName: "visible" }] : []));
|
|
612
|
+
task = input(null, ...(ngDevMode ? [{ debugName: "task" }] : []));
|
|
613
|
+
boardId = input.required(...(ngDevMode ? [{ debugName: "boardId" }] : []));
|
|
614
|
+
listId = input.required(...(ngDevMode ? [{ debugName: "listId" }] : []));
|
|
615
|
+
members = input([], ...(ngDevMode ? [{ debugName: "members" }] : []));
|
|
616
|
+
labels = input([], ...(ngDevMode ? [{ debugName: "labels" }] : []));
|
|
617
|
+
saved = output();
|
|
618
|
+
isSaving = signal(false, ...(ngDevMode ? [{ debugName: "isSaving" }] : []));
|
|
619
|
+
priorityOptions = PRIORITY_OPTIONS;
|
|
620
|
+
title = '';
|
|
621
|
+
description = '';
|
|
622
|
+
priority = 'MEDIUM';
|
|
623
|
+
dueDate = null;
|
|
624
|
+
selectedAssigneeIds = [];
|
|
625
|
+
selectedLabelIds = [];
|
|
626
|
+
constructor() {
|
|
627
|
+
effect(() => {
|
|
628
|
+
const t = this.task();
|
|
629
|
+
if (t) {
|
|
630
|
+
this.title = t.title;
|
|
631
|
+
this.description = t.description ?? '';
|
|
632
|
+
this.priority = t.priority;
|
|
633
|
+
this.dueDate = t.dueDate ? new Date(t.dueDate) : null;
|
|
634
|
+
this.selectedAssigneeIds = t.assignees?.map((a) => a.userId) ?? [];
|
|
635
|
+
this.selectedLabelIds = t.labelIds ?? [];
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
this.reset();
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
async onSave() {
|
|
643
|
+
if (!this.title.trim())
|
|
644
|
+
return;
|
|
645
|
+
this.isSaving.set(true);
|
|
646
|
+
try {
|
|
647
|
+
const dueDateStr = this.dueDate
|
|
648
|
+
? this.dueDate.toISOString().slice(0, 10)
|
|
649
|
+
: undefined;
|
|
650
|
+
const existing = this.task();
|
|
651
|
+
const priority = this.priority;
|
|
652
|
+
let result;
|
|
653
|
+
if (existing) {
|
|
654
|
+
const dto = {
|
|
655
|
+
id: existing.id,
|
|
656
|
+
title: this.title,
|
|
657
|
+
description: this.description || undefined,
|
|
658
|
+
priority,
|
|
659
|
+
dueDate: dueDateStr,
|
|
660
|
+
assigneeIds: this.selectedAssigneeIds,
|
|
661
|
+
labelIds: this.selectedLabelIds,
|
|
662
|
+
};
|
|
663
|
+
result = await this.taskApi.update(dto);
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
const dto = {
|
|
667
|
+
boardId: this.boardId(),
|
|
668
|
+
listId: this.listId(),
|
|
669
|
+
title: this.title,
|
|
670
|
+
description: this.description || undefined,
|
|
671
|
+
priority,
|
|
672
|
+
dueDate: dueDateStr,
|
|
673
|
+
assigneeIds: this.selectedAssigneeIds,
|
|
674
|
+
labelIds: this.selectedLabelIds,
|
|
675
|
+
};
|
|
676
|
+
result = await this.taskApi.insert(dto);
|
|
677
|
+
}
|
|
678
|
+
if (result.data)
|
|
679
|
+
this.saved.emit(result.data);
|
|
680
|
+
this.visible.set(false);
|
|
681
|
+
}
|
|
682
|
+
finally {
|
|
683
|
+
this.isSaving.set(false);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
onClose() {
|
|
687
|
+
this.visible.set(false);
|
|
688
|
+
this.reset();
|
|
689
|
+
}
|
|
690
|
+
reset() {
|
|
691
|
+
this.title = '';
|
|
692
|
+
this.description = '';
|
|
693
|
+
this.priority = 'MEDIUM';
|
|
694
|
+
this.dueDate = null;
|
|
695
|
+
this.selectedAssigneeIds = [];
|
|
696
|
+
this.selectedLabelIds = [];
|
|
697
|
+
}
|
|
698
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskFormDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
699
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.5", type: TaskFormDialogComponent, isStandalone: true, selector: "tm-task-form-dialog", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, task: { classPropertyName: "task", publicName: "task", isSignal: true, isRequired: false, transformFunction: null }, boardId: { classPropertyName: "boardId", publicName: "boardId", isSignal: true, isRequired: true, transformFunction: null }, listId: { classPropertyName: "listId", publicName: "listId", isSignal: true, isRequired: true, transformFunction: null }, members: { classPropertyName: "members", publicName: "members", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { visible: "visibleChange", saved: "saved" }, ngImport: i0, template: `
|
|
700
|
+
<p-dialog
|
|
701
|
+
[header]="
|
|
702
|
+
(task() ? 'task.form.title.edit' : 'task.form.title.create') | translate
|
|
703
|
+
"
|
|
704
|
+
[(visible)]="visible"
|
|
705
|
+
[modal]="true"
|
|
706
|
+
[style]="{ width: '540px' }"
|
|
707
|
+
(onHide)="onClose()"
|
|
708
|
+
>
|
|
709
|
+
<div class="flex flex-col gap-4 py-3">
|
|
710
|
+
<div class="flex flex-col gap-1">
|
|
711
|
+
<label class="text-sm font-medium"
|
|
712
|
+
>{{ 'task.form.title.label' | translate }}
|
|
713
|
+
<span class="text-red-500">*</span></label
|
|
714
|
+
>
|
|
715
|
+
<input
|
|
716
|
+
pInputText
|
|
717
|
+
[(ngModel)]="title"
|
|
718
|
+
[placeholder]="'task.form.title.placeholder' | translate"
|
|
719
|
+
/>
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
<div class="flex flex-col gap-1">
|
|
723
|
+
<label class="text-sm font-medium">{{
|
|
724
|
+
'shared.description' | translate
|
|
725
|
+
}}</label>
|
|
726
|
+
<textarea
|
|
727
|
+
pTextarea
|
|
728
|
+
[(ngModel)]="description"
|
|
729
|
+
rows="3"
|
|
730
|
+
[placeholder]="'task.form.description.placeholder' | translate"
|
|
731
|
+
></textarea>
|
|
732
|
+
</div>
|
|
733
|
+
|
|
734
|
+
<div class="grid grid-cols-2 gap-4">
|
|
735
|
+
<div class="flex flex-col gap-1">
|
|
736
|
+
<label class="text-sm font-medium">{{
|
|
737
|
+
'task.form.priority.label' | translate
|
|
738
|
+
}}</label>
|
|
739
|
+
<p-select
|
|
740
|
+
[(ngModel)]="priority"
|
|
741
|
+
[options]="priorityOptions"
|
|
742
|
+
optionLabel="label"
|
|
743
|
+
optionValue="value"
|
|
744
|
+
[placeholder]="'task.form.priority.placeholder' | translate"
|
|
745
|
+
>
|
|
746
|
+
<ng-template #selectedItem let-opt>
|
|
747
|
+
{{ opt.label | translate }}
|
|
748
|
+
</ng-template>
|
|
749
|
+
<ng-template #item let-opt>
|
|
750
|
+
{{ opt.label | translate }}
|
|
751
|
+
</ng-template>
|
|
752
|
+
</p-select>
|
|
753
|
+
</div>
|
|
754
|
+
|
|
755
|
+
<div class="flex flex-col gap-1">
|
|
756
|
+
<label class="text-sm font-medium">{{
|
|
757
|
+
'task.form.due.date.label' | translate
|
|
758
|
+
}}</label>
|
|
759
|
+
<p-datepicker
|
|
760
|
+
[(ngModel)]="dueDate"
|
|
761
|
+
[showClear]="true"
|
|
762
|
+
[placeholder]="'task.form.due.date.placeholder' | translate"
|
|
763
|
+
/>
|
|
764
|
+
</div>
|
|
765
|
+
</div>
|
|
766
|
+
|
|
767
|
+
<div class="flex flex-col gap-1">
|
|
768
|
+
<label class="text-sm font-medium">{{
|
|
769
|
+
'task.form.assignee.label' | translate
|
|
770
|
+
}}</label>
|
|
771
|
+
<p-multiselect
|
|
772
|
+
[(ngModel)]="selectedAssigneeIds"
|
|
773
|
+
[options]="members()"
|
|
774
|
+
optionLabel="userName"
|
|
775
|
+
optionValue="userId"
|
|
776
|
+
appendTo="body"
|
|
777
|
+
[placeholder]="'task.form.assignee.placeholder' | translate"
|
|
778
|
+
[showClear]="true"
|
|
779
|
+
/>
|
|
780
|
+
</div>
|
|
781
|
+
|
|
782
|
+
<div class="flex flex-col gap-1">
|
|
783
|
+
<label class="text-sm font-medium">{{
|
|
784
|
+
'task.form.labels.label' | translate
|
|
785
|
+
}}</label>
|
|
786
|
+
<p-multiselect
|
|
787
|
+
[(ngModel)]="selectedLabelIds"
|
|
788
|
+
[options]="labels()"
|
|
789
|
+
optionLabel="name"
|
|
790
|
+
optionValue="id"
|
|
791
|
+
appendTo="body"
|
|
792
|
+
[placeholder]="'task.form.labels.placeholder' | translate"
|
|
793
|
+
/>
|
|
794
|
+
</div>
|
|
795
|
+
</div>
|
|
796
|
+
|
|
797
|
+
<ng-template pTemplate="footer">
|
|
798
|
+
<div class="flex justify-end gap-2 pt-1">
|
|
799
|
+
<p-button
|
|
800
|
+
[label]="'shared.cancel' | translate"
|
|
801
|
+
severity="secondary"
|
|
802
|
+
(onClick)="onClose()"
|
|
803
|
+
/>
|
|
804
|
+
<p-button
|
|
805
|
+
[label]="'shared.save' | translate"
|
|
806
|
+
[loading]="isSaving()"
|
|
807
|
+
(onClick)="onSave()"
|
|
808
|
+
/>
|
|
809
|
+
</div>
|
|
810
|
+
</ng-template>
|
|
811
|
+
</p-dialog>
|
|
812
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: DialogModule }, { kind: "component", type: i1$1.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "directive", type: i2$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i4.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: TextareaModule }, { kind: "directive", type: i5.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["pTextareaPT", "pTextareaUnstyled", "autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i6.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: DatePickerModule }, { kind: "component", type: i7.DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "styleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "icon", "readonlyInput", "shortYearCutoff", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "minDate", "maxDate", "disabledDates", "disabledDays", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "view", "defaultDate", "appendTo", "motionOptions"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i8.MultiSelect, selector: "p-multiSelect, p-multiselect, p-multi-select", inputs: ["id", "ariaLabel", "styleClass", "panelStyle", "panelStyleClass", "inputId", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "dataKey", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "chipIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "showClear", "autofocus", "placeholder", "options", "filterValue", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus", "highlightOnSelect", "size", "variant", "fluid", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i9.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
|
|
813
|
+
}
|
|
814
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskFormDialogComponent, decorators: [{
|
|
815
|
+
type: Component,
|
|
816
|
+
args: [{
|
|
817
|
+
selector: 'tm-task-form-dialog',
|
|
818
|
+
imports: [
|
|
819
|
+
DialogModule,
|
|
820
|
+
FormsModule,
|
|
821
|
+
InputTextModule,
|
|
822
|
+
TextareaModule,
|
|
823
|
+
SelectModule,
|
|
824
|
+
DatePickerModule,
|
|
825
|
+
MultiSelectModule,
|
|
826
|
+
ButtonModule,
|
|
827
|
+
TranslatePipe,
|
|
828
|
+
],
|
|
829
|
+
template: `
|
|
830
|
+
<p-dialog
|
|
831
|
+
[header]="
|
|
832
|
+
(task() ? 'task.form.title.edit' : 'task.form.title.create') | translate
|
|
833
|
+
"
|
|
834
|
+
[(visible)]="visible"
|
|
835
|
+
[modal]="true"
|
|
836
|
+
[style]="{ width: '540px' }"
|
|
837
|
+
(onHide)="onClose()"
|
|
838
|
+
>
|
|
839
|
+
<div class="flex flex-col gap-4 py-3">
|
|
840
|
+
<div class="flex flex-col gap-1">
|
|
841
|
+
<label class="text-sm font-medium"
|
|
842
|
+
>{{ 'task.form.title.label' | translate }}
|
|
843
|
+
<span class="text-red-500">*</span></label
|
|
844
|
+
>
|
|
845
|
+
<input
|
|
846
|
+
pInputText
|
|
847
|
+
[(ngModel)]="title"
|
|
848
|
+
[placeholder]="'task.form.title.placeholder' | translate"
|
|
849
|
+
/>
|
|
850
|
+
</div>
|
|
851
|
+
|
|
852
|
+
<div class="flex flex-col gap-1">
|
|
853
|
+
<label class="text-sm font-medium">{{
|
|
854
|
+
'shared.description' | translate
|
|
855
|
+
}}</label>
|
|
856
|
+
<textarea
|
|
857
|
+
pTextarea
|
|
858
|
+
[(ngModel)]="description"
|
|
859
|
+
rows="3"
|
|
860
|
+
[placeholder]="'task.form.description.placeholder' | translate"
|
|
861
|
+
></textarea>
|
|
862
|
+
</div>
|
|
863
|
+
|
|
864
|
+
<div class="grid grid-cols-2 gap-4">
|
|
865
|
+
<div class="flex flex-col gap-1">
|
|
866
|
+
<label class="text-sm font-medium">{{
|
|
867
|
+
'task.form.priority.label' | translate
|
|
868
|
+
}}</label>
|
|
869
|
+
<p-select
|
|
870
|
+
[(ngModel)]="priority"
|
|
871
|
+
[options]="priorityOptions"
|
|
872
|
+
optionLabel="label"
|
|
873
|
+
optionValue="value"
|
|
874
|
+
[placeholder]="'task.form.priority.placeholder' | translate"
|
|
875
|
+
>
|
|
876
|
+
<ng-template #selectedItem let-opt>
|
|
877
|
+
{{ opt.label | translate }}
|
|
878
|
+
</ng-template>
|
|
879
|
+
<ng-template #item let-opt>
|
|
880
|
+
{{ opt.label | translate }}
|
|
881
|
+
</ng-template>
|
|
882
|
+
</p-select>
|
|
883
|
+
</div>
|
|
884
|
+
|
|
885
|
+
<div class="flex flex-col gap-1">
|
|
886
|
+
<label class="text-sm font-medium">{{
|
|
887
|
+
'task.form.due.date.label' | translate
|
|
888
|
+
}}</label>
|
|
889
|
+
<p-datepicker
|
|
890
|
+
[(ngModel)]="dueDate"
|
|
891
|
+
[showClear]="true"
|
|
892
|
+
[placeholder]="'task.form.due.date.placeholder' | translate"
|
|
893
|
+
/>
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
|
|
897
|
+
<div class="flex flex-col gap-1">
|
|
898
|
+
<label class="text-sm font-medium">{{
|
|
899
|
+
'task.form.assignee.label' | translate
|
|
900
|
+
}}</label>
|
|
901
|
+
<p-multiselect
|
|
902
|
+
[(ngModel)]="selectedAssigneeIds"
|
|
903
|
+
[options]="members()"
|
|
904
|
+
optionLabel="userName"
|
|
905
|
+
optionValue="userId"
|
|
906
|
+
appendTo="body"
|
|
907
|
+
[placeholder]="'task.form.assignee.placeholder' | translate"
|
|
908
|
+
[showClear]="true"
|
|
909
|
+
/>
|
|
910
|
+
</div>
|
|
911
|
+
|
|
912
|
+
<div class="flex flex-col gap-1">
|
|
913
|
+
<label class="text-sm font-medium">{{
|
|
914
|
+
'task.form.labels.label' | translate
|
|
915
|
+
}}</label>
|
|
916
|
+
<p-multiselect
|
|
917
|
+
[(ngModel)]="selectedLabelIds"
|
|
918
|
+
[options]="labels()"
|
|
919
|
+
optionLabel="name"
|
|
920
|
+
optionValue="id"
|
|
921
|
+
appendTo="body"
|
|
922
|
+
[placeholder]="'task.form.labels.placeholder' | translate"
|
|
923
|
+
/>
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
926
|
+
|
|
927
|
+
<ng-template pTemplate="footer">
|
|
928
|
+
<div class="flex justify-end gap-2 pt-1">
|
|
929
|
+
<p-button
|
|
930
|
+
[label]="'shared.cancel' | translate"
|
|
931
|
+
severity="secondary"
|
|
932
|
+
(onClick)="onClose()"
|
|
933
|
+
/>
|
|
934
|
+
<p-button
|
|
935
|
+
[label]="'shared.save' | translate"
|
|
936
|
+
[loading]="isSaving()"
|
|
937
|
+
(onClick)="onSave()"
|
|
938
|
+
/>
|
|
939
|
+
</div>
|
|
940
|
+
</ng-template>
|
|
941
|
+
</p-dialog>
|
|
942
|
+
`,
|
|
943
|
+
}]
|
|
944
|
+
}], ctorParameters: () => [], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }, { type: i0.Output, args: ["visibleChange"] }], task: [{ type: i0.Input, args: [{ isSignal: true, alias: "task", required: false }] }], boardId: [{ type: i0.Input, args: [{ isSignal: true, alias: "boardId", required: true }] }], listId: [{ type: i0.Input, args: [{ isSignal: true, alias: "listId", required: true }] }], members: [{ type: i0.Input, args: [{ isSignal: true, alias: "members", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], saved: [{ type: i0.Output, args: ["saved"] }] } });
|
|
945
|
+
|
|
946
|
+
class TaskDetailPanelComponent {
|
|
947
|
+
commentApi = inject(TaskCommentApiService);
|
|
948
|
+
state = inject(TaskBoardStateService);
|
|
949
|
+
task = input(null, ...(ngDevMode ? [{ debugName: "task" }] : []));
|
|
950
|
+
closePanel = output();
|
|
951
|
+
editTask = output();
|
|
952
|
+
comments = signal([], ...(ngDevMode ? [{ debugName: "comments" }] : []));
|
|
953
|
+
isSendingComment = signal(false, ...(ngDevMode ? [{ debugName: "isSendingComment" }] : []));
|
|
954
|
+
isSendingReply = signal(false, ...(ngDevMode ? [{ debugName: "isSendingReply" }] : []));
|
|
955
|
+
replyingToId = signal(null, ...(ngDevMode ? [{ debugName: "replyingToId" }] : []));
|
|
956
|
+
isDeleting = signal(false, ...(ngDevMode ? [{ debugName: "isDeleting" }] : []));
|
|
957
|
+
newComment = '';
|
|
958
|
+
replyText = '';
|
|
959
|
+
assigneeMembers = computed(() => this.task()?.assignees ?? [], ...(ngDevMode ? [{ debugName: "assigneeMembers" }] : []));
|
|
960
|
+
rootComments = computed(() => this.comments().filter((c) => !c.parentCommentId), ...(ngDevMode ? [{ debugName: "rootComments" }] : []));
|
|
961
|
+
repliesFor(parentId) {
|
|
962
|
+
return this.comments().filter((c) => c.parentCommentId === parentId);
|
|
963
|
+
}
|
|
964
|
+
constructor() {
|
|
965
|
+
effect(() => {
|
|
966
|
+
const t = this.task();
|
|
967
|
+
if (t)
|
|
968
|
+
this.loadComments(t.id);
|
|
969
|
+
else {
|
|
970
|
+
this.comments.set([]);
|
|
971
|
+
this.replyingToId.set(null);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
toggleReply(commentId) {
|
|
976
|
+
if (this.replyingToId() === commentId) {
|
|
977
|
+
this.cancelReply();
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
this.replyingToId.set(commentId);
|
|
981
|
+
this.replyText = '';
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
cancelReply() {
|
|
985
|
+
this.replyingToId.set(null);
|
|
986
|
+
this.replyText = '';
|
|
987
|
+
}
|
|
988
|
+
async deleteTask(taskId) {
|
|
989
|
+
this.isDeleting.set(true);
|
|
990
|
+
try {
|
|
991
|
+
await this.state.deleteTask(taskId);
|
|
992
|
+
this.closePanel.emit();
|
|
993
|
+
}
|
|
994
|
+
finally {
|
|
995
|
+
this.isDeleting.set(false);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
prioritySeverity = taskPrioritySeverity;
|
|
999
|
+
initials = memberInitials;
|
|
1000
|
+
async loadComments(taskId) {
|
|
1001
|
+
const res = await this.commentApi.getAll({ filter: { taskId } });
|
|
1002
|
+
this.comments.set(res.data ?? []);
|
|
1003
|
+
}
|
|
1004
|
+
async addComment(taskId) {
|
|
1005
|
+
if (!this.newComment.trim())
|
|
1006
|
+
return;
|
|
1007
|
+
this.isSendingComment.set(true);
|
|
1008
|
+
try {
|
|
1009
|
+
const res = await this.commentApi.insert({
|
|
1010
|
+
taskId,
|
|
1011
|
+
content: this.newComment,
|
|
1012
|
+
});
|
|
1013
|
+
if (res.data)
|
|
1014
|
+
this.comments.update((list) => [...list, res.data]);
|
|
1015
|
+
this.newComment = '';
|
|
1016
|
+
}
|
|
1017
|
+
finally {
|
|
1018
|
+
this.isSendingComment.set(false);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
async submitReply(taskId, parentCommentId) {
|
|
1022
|
+
if (!this.replyText.trim())
|
|
1023
|
+
return;
|
|
1024
|
+
this.isSendingReply.set(true);
|
|
1025
|
+
try {
|
|
1026
|
+
const res = await this.commentApi.insert({
|
|
1027
|
+
taskId,
|
|
1028
|
+
content: this.replyText,
|
|
1029
|
+
parentCommentId,
|
|
1030
|
+
});
|
|
1031
|
+
if (res.data)
|
|
1032
|
+
this.comments.update((list) => [...list, res.data]);
|
|
1033
|
+
this.cancelReply();
|
|
1034
|
+
}
|
|
1035
|
+
finally {
|
|
1036
|
+
this.isSendingReply.set(false);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskDetailPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1040
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: TaskDetailPanelComponent, isStandalone: true, selector: "tm-task-detail-panel", inputs: { task: { classPropertyName: "task", publicName: "task", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closePanel: "closePanel", editTask: "editTask" }, ngImport: i0, template: `
|
|
1041
|
+
<p-drawer
|
|
1042
|
+
[visible]="!!task()"
|
|
1043
|
+
position="right"
|
|
1044
|
+
[style]="{ width: '500px' }"
|
|
1045
|
+
[header]="task()?.title ?? ''"
|
|
1046
|
+
(onHide)="closePanel.emit()"
|
|
1047
|
+
>
|
|
1048
|
+
@if (task(); as t) {
|
|
1049
|
+
<div class="flex flex-col gap-6 h-full overflow-y-auto pb-4">
|
|
1050
|
+
<div class="flex items-center justify-end gap-1">
|
|
1051
|
+
<p-button
|
|
1052
|
+
icon="pi pi-pencil"
|
|
1053
|
+
severity="secondary"
|
|
1054
|
+
size="small"
|
|
1055
|
+
[text]="true"
|
|
1056
|
+
[pTooltip]="'task.detail.edit' | translate"
|
|
1057
|
+
tooltipPosition="left"
|
|
1058
|
+
(onClick)="editTask.emit(t)"
|
|
1059
|
+
/>
|
|
1060
|
+
<p-button
|
|
1061
|
+
icon="pi pi-trash"
|
|
1062
|
+
severity="danger"
|
|
1063
|
+
size="small"
|
|
1064
|
+
[text]="true"
|
|
1065
|
+
[loading]="isDeleting()"
|
|
1066
|
+
[pTooltip]="'task.detail.delete' | translate"
|
|
1067
|
+
tooltipPosition="left"
|
|
1068
|
+
(onClick)="deleteTask(t.id)"
|
|
1069
|
+
/>
|
|
1070
|
+
</div>
|
|
1071
|
+
|
|
1072
|
+
<div class="flex items-center gap-3">
|
|
1073
|
+
<p-tag
|
|
1074
|
+
[value]="t.priority"
|
|
1075
|
+
[severity]="prioritySeverity(t.priority)"
|
|
1076
|
+
/>
|
|
1077
|
+
@if (t.dueDate) {
|
|
1078
|
+
<span class="text-sm text-surface-500">
|
|
1079
|
+
{{ 'task.detail.due.label' | translate }}:
|
|
1080
|
+
{{ t.dueDate | date: 'mediumDate' }}
|
|
1081
|
+
</span>
|
|
1082
|
+
}
|
|
1083
|
+
</div>
|
|
1084
|
+
|
|
1085
|
+
@if (assigneeMembers().length > 0) {
|
|
1086
|
+
<div>
|
|
1087
|
+
<h4 class="text-sm font-semibold mb-2">
|
|
1088
|
+
{{ 'task.detail.assignees.label' | translate }}
|
|
1089
|
+
</h4>
|
|
1090
|
+
<div class="flex flex-wrap gap-2">
|
|
1091
|
+
@for (member of assigneeMembers(); track member.userId) {
|
|
1092
|
+
<div class="flex items-center gap-2 px-2 py-1 rounded-full ">
|
|
1093
|
+
<span
|
|
1094
|
+
class="w-6 h-6 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300 flex items-center justify-center text-xs font-semibold"
|
|
1095
|
+
>
|
|
1096
|
+
{{ initials(member.userName) }}
|
|
1097
|
+
</span>
|
|
1098
|
+
<span class="text-sm">{{ member.userName }}</span>
|
|
1099
|
+
</div>
|
|
1100
|
+
}
|
|
1101
|
+
</div>
|
|
1102
|
+
</div>
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
<div>
|
|
1106
|
+
<h4 class="text-sm font-semibold mb-2">
|
|
1107
|
+
{{ 'shared.description' | translate }}
|
|
1108
|
+
</h4>
|
|
1109
|
+
<p
|
|
1110
|
+
class="text-sm text-surface-600 dark:text-surface-400 whitespace-pre-wrap"
|
|
1111
|
+
>
|
|
1112
|
+
{{
|
|
1113
|
+
t.description || ('task.detail.description.empty' | translate)
|
|
1114
|
+
}}
|
|
1115
|
+
</p>
|
|
1116
|
+
</div>
|
|
1117
|
+
|
|
1118
|
+
<div class="flex flex-col gap-1">
|
|
1119
|
+
<h4 class="text-sm font-semibold mb-1">
|
|
1120
|
+
{{ 'task.detail.comments.label' | translate }} ({{
|
|
1121
|
+
rootComments().length
|
|
1122
|
+
}})
|
|
1123
|
+
</h4>
|
|
1124
|
+
|
|
1125
|
+
@for (comment of rootComments(); track comment.id) {
|
|
1126
|
+
<div
|
|
1127
|
+
class="group flex flex-col gap-1 py-2 border-b border-surface-100 dark:border-surface-800 last:border-0"
|
|
1128
|
+
>
|
|
1129
|
+
<div class="flex items-center justify-between">
|
|
1130
|
+
<span class="text-xs text-surface-400">{{
|
|
1131
|
+
comment.createdAt | date: 'MMM d, y, h:mm a'
|
|
1132
|
+
}}</span>
|
|
1133
|
+
<p-button
|
|
1134
|
+
[label]="'task.comment.reply' | translate"
|
|
1135
|
+
icon="pi pi-reply"
|
|
1136
|
+
size="small"
|
|
1137
|
+
[text]="true"
|
|
1138
|
+
severity="secondary"
|
|
1139
|
+
styleClass="opacity-0 group-hover:opacity-100 transition-opacity !py-0.5 !px-1.5 text-xs"
|
|
1140
|
+
(onClick)="toggleReply(comment.id)"
|
|
1141
|
+
/>
|
|
1142
|
+
</div>
|
|
1143
|
+
<p class="text-sm">{{ comment.content }}</p>
|
|
1144
|
+
|
|
1145
|
+
@if (repliesFor(comment.id).length > 0) {
|
|
1146
|
+
<div
|
|
1147
|
+
class="mt-1 pl-3 border-l-2 border-surface-200 dark:border-surface-700 flex flex-col gap-2"
|
|
1148
|
+
>
|
|
1149
|
+
@for (reply of repliesFor(comment.id); track reply.id) {
|
|
1150
|
+
<div class="flex flex-col gap-0.5">
|
|
1151
|
+
<span class="text-xs text-surface-400">{{
|
|
1152
|
+
reply.createdAt | date: 'MMM d, y, h:mm a'
|
|
1153
|
+
}}</span>
|
|
1154
|
+
<p class="text-sm">{{ reply.content }}</p>
|
|
1155
|
+
</div>
|
|
1156
|
+
}
|
|
1157
|
+
</div>
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
@if (replyingToId() === comment.id) {
|
|
1161
|
+
<div
|
|
1162
|
+
class="mt-2 pl-3 border-l-2 border-primary-300 dark:border-primary-700 flex gap-2"
|
|
1163
|
+
>
|
|
1164
|
+
<textarea
|
|
1165
|
+
pTextarea
|
|
1166
|
+
[(ngModel)]="replyText"
|
|
1167
|
+
rows="2"
|
|
1168
|
+
[placeholder]="
|
|
1169
|
+
'task.comment.reply.placeholder' | translate
|
|
1170
|
+
"
|
|
1171
|
+
class="flex-1 resize-none text-sm"
|
|
1172
|
+
autoResize="true"
|
|
1173
|
+
></textarea>
|
|
1174
|
+
<div class="flex flex-col gap-1">
|
|
1175
|
+
<p-button
|
|
1176
|
+
icon="pi pi-send"
|
|
1177
|
+
size="small"
|
|
1178
|
+
[loading]="isSendingReply()"
|
|
1179
|
+
[disabled]="!replyText.trim()"
|
|
1180
|
+
(onClick)="submitReply(t.id, comment.id)"
|
|
1181
|
+
/>
|
|
1182
|
+
<p-button
|
|
1183
|
+
icon="pi pi-times"
|
|
1184
|
+
size="small"
|
|
1185
|
+
severity="secondary"
|
|
1186
|
+
[text]="true"
|
|
1187
|
+
(onClick)="cancelReply()"
|
|
1188
|
+
/>
|
|
1189
|
+
</div>
|
|
1190
|
+
</div>
|
|
1191
|
+
}
|
|
1192
|
+
</div>
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
@if (rootComments().length === 0) {
|
|
1196
|
+
<p class="text-sm text-surface-400 py-2">
|
|
1197
|
+
{{ 'task.detail.comments.empty' | translate }}
|
|
1198
|
+
</p>
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
<div class="flex gap-2 mt-3">
|
|
1202
|
+
<textarea
|
|
1203
|
+
pTextarea
|
|
1204
|
+
[(ngModel)]="newComment"
|
|
1205
|
+
rows="2"
|
|
1206
|
+
[placeholder]="'task.detail.comment.placeholder' | translate"
|
|
1207
|
+
class="flex-1 resize-none"
|
|
1208
|
+
></textarea>
|
|
1209
|
+
<p-button
|
|
1210
|
+
icon="pi pi-send"
|
|
1211
|
+
[loading]="isSendingComment()"
|
|
1212
|
+
[disabled]="!newComment.trim()"
|
|
1213
|
+
(onClick)="addComment(t.id)"
|
|
1214
|
+
/>
|
|
1215
|
+
</div>
|
|
1216
|
+
</div>
|
|
1217
|
+
</div>
|
|
1218
|
+
}
|
|
1219
|
+
</p-drawer>
|
|
1220
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: DrawerModule }, { kind: "component", type: i1$2.Drawer, selector: "p-drawer", inputs: ["appendTo", "motionOptions", "blockScroll", "style", "styleClass", "ariaCloseLabel", "autoZIndex", "baseZIndex", "modal", "closeButtonProps", "dismissible", "showCloseIcon", "closeOnEscape", "transitionOptions", "visible", "position", "fullScreen", "header", "maskStyle", "closable"], outputs: ["onShow", "onHide", "visibleChange"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i9.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i1.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TextareaModule }, { kind: "directive", type: i5.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["pTextareaPT", "pTextareaUnstyled", "autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i2.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "pipe", type: DatePipe, name: "date" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
|
|
1221
|
+
}
|
|
1222
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskDetailPanelComponent, decorators: [{
|
|
1223
|
+
type: Component,
|
|
1224
|
+
args: [{
|
|
1225
|
+
selector: 'tm-task-detail-panel',
|
|
1226
|
+
imports: [
|
|
1227
|
+
DrawerModule,
|
|
1228
|
+
ButtonModule,
|
|
1229
|
+
TagModule,
|
|
1230
|
+
DatePipe,
|
|
1231
|
+
FormsModule,
|
|
1232
|
+
TextareaModule,
|
|
1233
|
+
TooltipModule,
|
|
1234
|
+
TranslatePipe,
|
|
1235
|
+
],
|
|
1236
|
+
template: `
|
|
1237
|
+
<p-drawer
|
|
1238
|
+
[visible]="!!task()"
|
|
1239
|
+
position="right"
|
|
1240
|
+
[style]="{ width: '500px' }"
|
|
1241
|
+
[header]="task()?.title ?? ''"
|
|
1242
|
+
(onHide)="closePanel.emit()"
|
|
1243
|
+
>
|
|
1244
|
+
@if (task(); as t) {
|
|
1245
|
+
<div class="flex flex-col gap-6 h-full overflow-y-auto pb-4">
|
|
1246
|
+
<div class="flex items-center justify-end gap-1">
|
|
1247
|
+
<p-button
|
|
1248
|
+
icon="pi pi-pencil"
|
|
1249
|
+
severity="secondary"
|
|
1250
|
+
size="small"
|
|
1251
|
+
[text]="true"
|
|
1252
|
+
[pTooltip]="'task.detail.edit' | translate"
|
|
1253
|
+
tooltipPosition="left"
|
|
1254
|
+
(onClick)="editTask.emit(t)"
|
|
1255
|
+
/>
|
|
1256
|
+
<p-button
|
|
1257
|
+
icon="pi pi-trash"
|
|
1258
|
+
severity="danger"
|
|
1259
|
+
size="small"
|
|
1260
|
+
[text]="true"
|
|
1261
|
+
[loading]="isDeleting()"
|
|
1262
|
+
[pTooltip]="'task.detail.delete' | translate"
|
|
1263
|
+
tooltipPosition="left"
|
|
1264
|
+
(onClick)="deleteTask(t.id)"
|
|
1265
|
+
/>
|
|
1266
|
+
</div>
|
|
1267
|
+
|
|
1268
|
+
<div class="flex items-center gap-3">
|
|
1269
|
+
<p-tag
|
|
1270
|
+
[value]="t.priority"
|
|
1271
|
+
[severity]="prioritySeverity(t.priority)"
|
|
1272
|
+
/>
|
|
1273
|
+
@if (t.dueDate) {
|
|
1274
|
+
<span class="text-sm text-surface-500">
|
|
1275
|
+
{{ 'task.detail.due.label' | translate }}:
|
|
1276
|
+
{{ t.dueDate | date: 'mediumDate' }}
|
|
1277
|
+
</span>
|
|
1278
|
+
}
|
|
1279
|
+
</div>
|
|
1280
|
+
|
|
1281
|
+
@if (assigneeMembers().length > 0) {
|
|
1282
|
+
<div>
|
|
1283
|
+
<h4 class="text-sm font-semibold mb-2">
|
|
1284
|
+
{{ 'task.detail.assignees.label' | translate }}
|
|
1285
|
+
</h4>
|
|
1286
|
+
<div class="flex flex-wrap gap-2">
|
|
1287
|
+
@for (member of assigneeMembers(); track member.userId) {
|
|
1288
|
+
<div class="flex items-center gap-2 px-2 py-1 rounded-full ">
|
|
1289
|
+
<span
|
|
1290
|
+
class="w-6 h-6 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300 flex items-center justify-center text-xs font-semibold"
|
|
1291
|
+
>
|
|
1292
|
+
{{ initials(member.userName) }}
|
|
1293
|
+
</span>
|
|
1294
|
+
<span class="text-sm">{{ member.userName }}</span>
|
|
1295
|
+
</div>
|
|
1296
|
+
}
|
|
1297
|
+
</div>
|
|
1298
|
+
</div>
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
<div>
|
|
1302
|
+
<h4 class="text-sm font-semibold mb-2">
|
|
1303
|
+
{{ 'shared.description' | translate }}
|
|
1304
|
+
</h4>
|
|
1305
|
+
<p
|
|
1306
|
+
class="text-sm text-surface-600 dark:text-surface-400 whitespace-pre-wrap"
|
|
1307
|
+
>
|
|
1308
|
+
{{
|
|
1309
|
+
t.description || ('task.detail.description.empty' | translate)
|
|
1310
|
+
}}
|
|
1311
|
+
</p>
|
|
1312
|
+
</div>
|
|
1313
|
+
|
|
1314
|
+
<div class="flex flex-col gap-1">
|
|
1315
|
+
<h4 class="text-sm font-semibold mb-1">
|
|
1316
|
+
{{ 'task.detail.comments.label' | translate }} ({{
|
|
1317
|
+
rootComments().length
|
|
1318
|
+
}})
|
|
1319
|
+
</h4>
|
|
1320
|
+
|
|
1321
|
+
@for (comment of rootComments(); track comment.id) {
|
|
1322
|
+
<div
|
|
1323
|
+
class="group flex flex-col gap-1 py-2 border-b border-surface-100 dark:border-surface-800 last:border-0"
|
|
1324
|
+
>
|
|
1325
|
+
<div class="flex items-center justify-between">
|
|
1326
|
+
<span class="text-xs text-surface-400">{{
|
|
1327
|
+
comment.createdAt | date: 'MMM d, y, h:mm a'
|
|
1328
|
+
}}</span>
|
|
1329
|
+
<p-button
|
|
1330
|
+
[label]="'task.comment.reply' | translate"
|
|
1331
|
+
icon="pi pi-reply"
|
|
1332
|
+
size="small"
|
|
1333
|
+
[text]="true"
|
|
1334
|
+
severity="secondary"
|
|
1335
|
+
styleClass="opacity-0 group-hover:opacity-100 transition-opacity !py-0.5 !px-1.5 text-xs"
|
|
1336
|
+
(onClick)="toggleReply(comment.id)"
|
|
1337
|
+
/>
|
|
1338
|
+
</div>
|
|
1339
|
+
<p class="text-sm">{{ comment.content }}</p>
|
|
1340
|
+
|
|
1341
|
+
@if (repliesFor(comment.id).length > 0) {
|
|
1342
|
+
<div
|
|
1343
|
+
class="mt-1 pl-3 border-l-2 border-surface-200 dark:border-surface-700 flex flex-col gap-2"
|
|
1344
|
+
>
|
|
1345
|
+
@for (reply of repliesFor(comment.id); track reply.id) {
|
|
1346
|
+
<div class="flex flex-col gap-0.5">
|
|
1347
|
+
<span class="text-xs text-surface-400">{{
|
|
1348
|
+
reply.createdAt | date: 'MMM d, y, h:mm a'
|
|
1349
|
+
}}</span>
|
|
1350
|
+
<p class="text-sm">{{ reply.content }}</p>
|
|
1351
|
+
</div>
|
|
1352
|
+
}
|
|
1353
|
+
</div>
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
@if (replyingToId() === comment.id) {
|
|
1357
|
+
<div
|
|
1358
|
+
class="mt-2 pl-3 border-l-2 border-primary-300 dark:border-primary-700 flex gap-2"
|
|
1359
|
+
>
|
|
1360
|
+
<textarea
|
|
1361
|
+
pTextarea
|
|
1362
|
+
[(ngModel)]="replyText"
|
|
1363
|
+
rows="2"
|
|
1364
|
+
[placeholder]="
|
|
1365
|
+
'task.comment.reply.placeholder' | translate
|
|
1366
|
+
"
|
|
1367
|
+
class="flex-1 resize-none text-sm"
|
|
1368
|
+
autoResize="true"
|
|
1369
|
+
></textarea>
|
|
1370
|
+
<div class="flex flex-col gap-1">
|
|
1371
|
+
<p-button
|
|
1372
|
+
icon="pi pi-send"
|
|
1373
|
+
size="small"
|
|
1374
|
+
[loading]="isSendingReply()"
|
|
1375
|
+
[disabled]="!replyText.trim()"
|
|
1376
|
+
(onClick)="submitReply(t.id, comment.id)"
|
|
1377
|
+
/>
|
|
1378
|
+
<p-button
|
|
1379
|
+
icon="pi pi-times"
|
|
1380
|
+
size="small"
|
|
1381
|
+
severity="secondary"
|
|
1382
|
+
[text]="true"
|
|
1383
|
+
(onClick)="cancelReply()"
|
|
1384
|
+
/>
|
|
1385
|
+
</div>
|
|
1386
|
+
</div>
|
|
1387
|
+
}
|
|
1388
|
+
</div>
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
@if (rootComments().length === 0) {
|
|
1392
|
+
<p class="text-sm text-surface-400 py-2">
|
|
1393
|
+
{{ 'task.detail.comments.empty' | translate }}
|
|
1394
|
+
</p>
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
<div class="flex gap-2 mt-3">
|
|
1398
|
+
<textarea
|
|
1399
|
+
pTextarea
|
|
1400
|
+
[(ngModel)]="newComment"
|
|
1401
|
+
rows="2"
|
|
1402
|
+
[placeholder]="'task.detail.comment.placeholder' | translate"
|
|
1403
|
+
class="flex-1 resize-none"
|
|
1404
|
+
></textarea>
|
|
1405
|
+
<p-button
|
|
1406
|
+
icon="pi pi-send"
|
|
1407
|
+
[loading]="isSendingComment()"
|
|
1408
|
+
[disabled]="!newComment.trim()"
|
|
1409
|
+
(onClick)="addComment(t.id)"
|
|
1410
|
+
/>
|
|
1411
|
+
</div>
|
|
1412
|
+
</div>
|
|
1413
|
+
</div>
|
|
1414
|
+
}
|
|
1415
|
+
</p-drawer>
|
|
1416
|
+
`,
|
|
1417
|
+
}]
|
|
1418
|
+
}], ctorParameters: () => [], propDecorators: { task: [{ type: i0.Input, args: [{ isSignal: true, alias: "task", required: false }] }], closePanel: [{ type: i0.Output, args: ["closePanel"] }], editTask: [{ type: i0.Output, args: ["editTask"] }] } });
|
|
1419
|
+
|
|
1420
|
+
class BoardListPageComponent {
|
|
1421
|
+
state = inject(TaskBoardStateService);
|
|
1422
|
+
boardApi = inject(TaskBoardApiService);
|
|
1423
|
+
router = inject(Router);
|
|
1424
|
+
showCreateDialog = signal(false, ...(ngDevMode ? [{ debugName: "showCreateDialog" }] : []));
|
|
1425
|
+
isCreating = signal(false, ...(ngDevMode ? [{ debugName: "isCreating" }] : []));
|
|
1426
|
+
newBoardName = '';
|
|
1427
|
+
newBoardDescription = '';
|
|
1428
|
+
newBoardColor = '#6366f1';
|
|
1429
|
+
boardColors = BOARD_COLOR_PALETTE;
|
|
1430
|
+
constructor() {
|
|
1431
|
+
this.state.loadBoards();
|
|
1432
|
+
}
|
|
1433
|
+
openBoard(boardId) {
|
|
1434
|
+
this.router.navigate(['/task-manager/board', boardId]);
|
|
1435
|
+
}
|
|
1436
|
+
closeCreateDialog() {
|
|
1437
|
+
this.showCreateDialog.set(false);
|
|
1438
|
+
this.newBoardName = '';
|
|
1439
|
+
this.newBoardDescription = '';
|
|
1440
|
+
this.newBoardColor = '#6366f1';
|
|
1441
|
+
}
|
|
1442
|
+
async createBoard() {
|
|
1443
|
+
if (!this.newBoardName.trim())
|
|
1444
|
+
return;
|
|
1445
|
+
this.isCreating.set(true);
|
|
1446
|
+
try {
|
|
1447
|
+
await this.boardApi.insert({
|
|
1448
|
+
name: this.newBoardName,
|
|
1449
|
+
description: this.newBoardDescription || undefined,
|
|
1450
|
+
color: this.newBoardColor,
|
|
1451
|
+
});
|
|
1452
|
+
await this.state.loadBoards();
|
|
1453
|
+
this.closeCreateDialog();
|
|
1454
|
+
}
|
|
1455
|
+
finally {
|
|
1456
|
+
this.isCreating.set(false);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BoardListPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1460
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: BoardListPageComponent, isStandalone: true, selector: "tm-board-list-page", ngImport: i0, template: `
|
|
1461
|
+
<div class="p-2 sm:p-4">
|
|
1462
|
+
<div
|
|
1463
|
+
class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-4"
|
|
1464
|
+
>
|
|
1465
|
+
<div>
|
|
1466
|
+
<h1 class="text-xl sm:text-2xl font-bold m-0">
|
|
1467
|
+
{{ 'task.manager.boards.title' | translate }}
|
|
1468
|
+
</h1>
|
|
1469
|
+
<p class="text-muted-color mt-1 text-sm sm:text-base">
|
|
1470
|
+
{{ 'task.manager.boards.subtitle' | translate }}
|
|
1471
|
+
</p>
|
|
1472
|
+
</div>
|
|
1473
|
+
<p-button
|
|
1474
|
+
[label]="'task.manager.board.new' | translate"
|
|
1475
|
+
icon="pi pi-plus"
|
|
1476
|
+
(onClick)="showCreateDialog.set(true)"
|
|
1477
|
+
/>
|
|
1478
|
+
</div>
|
|
1479
|
+
|
|
1480
|
+
@if (state.isLoading()) {
|
|
1481
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
1482
|
+
@for (i of [1, 2, 3, 4, 5, 6]; track i) {
|
|
1483
|
+
<div class="card !p-0 overflow-hidden">
|
|
1484
|
+
<p-skeleton height="4px" borderRadius="0" />
|
|
1485
|
+
<div class="p-5">
|
|
1486
|
+
<p-skeleton height="18px" width="60%" class="mb-3" />
|
|
1487
|
+
<p-skeleton height="13px" class="mb-1" />
|
|
1488
|
+
<p-skeleton height="13px" width="70%" class="mb-5" />
|
|
1489
|
+
<div class="flex gap-5 pt-4 border-t border-surface-100">
|
|
1490
|
+
<p-skeleton height="14px" width="55px" />
|
|
1491
|
+
<p-skeleton height="14px" width="45px" />
|
|
1492
|
+
<p-skeleton height="14px" width="50px" />
|
|
1493
|
+
</div>
|
|
1494
|
+
<p-skeleton height="11px" width="100px" class="mt-2" />
|
|
1495
|
+
</div>
|
|
1496
|
+
</div>
|
|
1497
|
+
}
|
|
1498
|
+
</div>
|
|
1499
|
+
} @else if (state.boards().length === 0) {
|
|
1500
|
+
<div
|
|
1501
|
+
class="flex flex-col items-center justify-center py-20 text-muted-color"
|
|
1502
|
+
>
|
|
1503
|
+
<span
|
|
1504
|
+
class="w-16 h-16 rounded-2xl border-2 border-dashed border-surface-300 flex items-center justify-center mb-4"
|
|
1505
|
+
>
|
|
1506
|
+
<i class="pi pi-th-large text-2xl text-surface-400"></i>
|
|
1507
|
+
</span>
|
|
1508
|
+
<p class="text-base font-semibold mb-1">
|
|
1509
|
+
{{ 'task.manager.board.empty' | translate }}
|
|
1510
|
+
</p>
|
|
1511
|
+
<p class="text-sm text-muted-color mb-5">
|
|
1512
|
+
{{ 'task.manager.board.empty.hint' | translate }}
|
|
1513
|
+
</p>
|
|
1514
|
+
<p-button
|
|
1515
|
+
[label]="'task.manager.board.new' | translate"
|
|
1516
|
+
icon="pi pi-plus"
|
|
1517
|
+
size="small"
|
|
1518
|
+
(onClick)="showCreateDialog.set(true)"
|
|
1519
|
+
/>
|
|
1520
|
+
</div>
|
|
1521
|
+
} @else {
|
|
1522
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
1523
|
+
@for (board of state.boards(); track board.id) {
|
|
1524
|
+
<div
|
|
1525
|
+
class="card !p-0 cursor-pointer overflow-hidden group hover:shadow-lg transition-all duration-200"
|
|
1526
|
+
(click)="openBoard(board.id)"
|
|
1527
|
+
>
|
|
1528
|
+
<div class="h-1" [style.background-color]="board.color"></div>
|
|
1529
|
+
|
|
1530
|
+
<div class="p-5">
|
|
1531
|
+
<div class="flex items-start justify-between gap-3 mb-2">
|
|
1532
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
1533
|
+
<h3
|
|
1534
|
+
class="font-semibold text-base m-0 truncate group-hover:text-primary-500 transition-colors duration-150"
|
|
1535
|
+
>
|
|
1536
|
+
{{ board.name }}
|
|
1537
|
+
</h3>
|
|
1538
|
+
</div>
|
|
1539
|
+
<i
|
|
1540
|
+
class="pi pi-arrow-right text-xs text-surface-300 group-hover:text-primary-500 group-hover:translate-x-0.5 transition-all duration-150 flex-shrink-0 mt-1"
|
|
1541
|
+
></i>
|
|
1542
|
+
</div>
|
|
1543
|
+
|
|
1544
|
+
<p
|
|
1545
|
+
class="text-sm text-muted-color line-clamp-2 pl-[18px] mb-0 min-h-[40px]"
|
|
1546
|
+
>
|
|
1547
|
+
{{ board.description || '' }}
|
|
1548
|
+
</p>
|
|
1549
|
+
|
|
1550
|
+
<div
|
|
1551
|
+
class="border-t border-surface-100 dark:border-surface-700 mt-4 pt-4 space-y-2.5"
|
|
1552
|
+
>
|
|
1553
|
+
<div class="flex items-center gap-4">
|
|
1554
|
+
<span class="flex items-center gap-1.5">
|
|
1555
|
+
<i class="pi pi-users text-[11px] text-muted-color"></i>
|
|
1556
|
+
<span class="text-sm font-semibold">{{
|
|
1557
|
+
board.totalMembers ?? 0
|
|
1558
|
+
}}</span>
|
|
1559
|
+
<span class="text-xs text-muted-color">{{
|
|
1560
|
+
'task.manager.board.members' | translate
|
|
1561
|
+
}}</span>
|
|
1562
|
+
</span>
|
|
1563
|
+
<span
|
|
1564
|
+
class="w-px h-3 bg-surface-200 dark:bg-surface-600"
|
|
1565
|
+
></span>
|
|
1566
|
+
<span class="flex items-center gap-1.5">
|
|
1567
|
+
<i class="pi pi-list text-[11px] text-muted-color"></i>
|
|
1568
|
+
<span class="text-sm font-semibold">{{
|
|
1569
|
+
board.totalLists ?? 0
|
|
1570
|
+
}}</span>
|
|
1571
|
+
<span class="text-xs text-muted-color">{{
|
|
1572
|
+
'task.manager.board.lists' | translate
|
|
1573
|
+
}}</span>
|
|
1574
|
+
</span>
|
|
1575
|
+
<span
|
|
1576
|
+
class="w-px h-3 bg-surface-200 dark:bg-surface-600"
|
|
1577
|
+
></span>
|
|
1578
|
+
<span class="flex items-center gap-1.5">
|
|
1579
|
+
<i
|
|
1580
|
+
class="pi pi-check-square text-[11px] text-muted-color"
|
|
1581
|
+
></i>
|
|
1582
|
+
<span class="text-sm font-semibold">{{
|
|
1583
|
+
board.totalTasks ?? 0
|
|
1584
|
+
}}</span>
|
|
1585
|
+
<span class="text-xs text-muted-color">{{
|
|
1586
|
+
'task.manager.board.tasks' | translate
|
|
1587
|
+
}}</span>
|
|
1588
|
+
</span>
|
|
1589
|
+
</div>
|
|
1590
|
+
<div
|
|
1591
|
+
class="flex items-center gap-1.5 text-xs text-muted-color"
|
|
1592
|
+
>
|
|
1593
|
+
<i class="pi pi-clock text-[10px]"></i>
|
|
1594
|
+
<span>{{ board.createdAt | date: 'MMM d, yyyy' }}</span>
|
|
1595
|
+
</div>
|
|
1596
|
+
</div>
|
|
1597
|
+
</div>
|
|
1598
|
+
</div>
|
|
1599
|
+
}
|
|
1600
|
+
</div>
|
|
1601
|
+
}
|
|
1602
|
+
</div>
|
|
1603
|
+
|
|
1604
|
+
<p-dialog
|
|
1605
|
+
[header]="'task.manager.board.new' | translate"
|
|
1606
|
+
[(visible)]="showCreateDialog"
|
|
1607
|
+
[modal]="true"
|
|
1608
|
+
[style]="{ width: '440px' }"
|
|
1609
|
+
>
|
|
1610
|
+
<div class="flex flex-col gap-4 py-2">
|
|
1611
|
+
<div class="flex flex-col gap-1">
|
|
1612
|
+
<label class="text-sm font-medium"
|
|
1613
|
+
>{{ 'shared.name' | translate }}
|
|
1614
|
+
<span class="text-red-500">*</span></label
|
|
1615
|
+
>
|
|
1616
|
+
<input
|
|
1617
|
+
pInputText
|
|
1618
|
+
[(ngModel)]="newBoardName"
|
|
1619
|
+
[placeholder]="'task.manager.board.name.placeholder' | translate"
|
|
1620
|
+
/>
|
|
1621
|
+
</div>
|
|
1622
|
+
<div class="flex flex-col gap-1">
|
|
1623
|
+
<label class="text-sm font-medium">{{
|
|
1624
|
+
'shared.description' | translate
|
|
1625
|
+
}}</label>
|
|
1626
|
+
<textarea
|
|
1627
|
+
pTextarea
|
|
1628
|
+
[(ngModel)]="newBoardDescription"
|
|
1629
|
+
rows="2"
|
|
1630
|
+
[placeholder]="
|
|
1631
|
+
'task.manager.board.description.placeholder' | translate
|
|
1632
|
+
"
|
|
1633
|
+
></textarea>
|
|
1634
|
+
</div>
|
|
1635
|
+
<div class="flex flex-col gap-2">
|
|
1636
|
+
<label class="text-sm font-medium">{{
|
|
1637
|
+
'task.manager.board.color.label' | translate
|
|
1638
|
+
}}</label>
|
|
1639
|
+
<div class="flex flex-wrap gap-2">
|
|
1640
|
+
@for (c of boardColors; track c) {
|
|
1641
|
+
<button
|
|
1642
|
+
type="button"
|
|
1643
|
+
class="w-8 h-8 rounded-full border-2 transition-all hover:scale-110 focus:outline-none flex items-center justify-center"
|
|
1644
|
+
[style.background-color]="c"
|
|
1645
|
+
[class.ring-2]="newBoardColor === c"
|
|
1646
|
+
[class.ring-offset-2]="newBoardColor === c"
|
|
1647
|
+
[class.ring-surface-900]="newBoardColor === c"
|
|
1648
|
+
[pTooltip]="c"
|
|
1649
|
+
(click)="newBoardColor = c"
|
|
1650
|
+
>
|
|
1651
|
+
@if (newBoardColor === c) {
|
|
1652
|
+
<i class="pi pi-check text-white text-xs"></i>
|
|
1653
|
+
}
|
|
1654
|
+
</button>
|
|
1655
|
+
}
|
|
1656
|
+
</div>
|
|
1657
|
+
<div class="flex items-center gap-2 mt-1">
|
|
1658
|
+
<span
|
|
1659
|
+
class="w-8 h-8 rounded-full border border-surface-200 flex-shrink-0"
|
|
1660
|
+
[style.background-color]="newBoardColor"
|
|
1661
|
+
></span>
|
|
1662
|
+
<span class="text-sm text-surface-500 font-mono">{{
|
|
1663
|
+
newBoardColor
|
|
1664
|
+
}}</span>
|
|
1665
|
+
</div>
|
|
1666
|
+
</div>
|
|
1667
|
+
</div>
|
|
1668
|
+
|
|
1669
|
+
<ng-template pTemplate="footer">
|
|
1670
|
+
<div class="flex justify-end gap-2">
|
|
1671
|
+
<p-button
|
|
1672
|
+
[label]="'shared.cancel' | translate"
|
|
1673
|
+
severity="secondary"
|
|
1674
|
+
(onClick)="closeCreateDialog()"
|
|
1675
|
+
/>
|
|
1676
|
+
<p-button
|
|
1677
|
+
[label]="'shared.create' | translate"
|
|
1678
|
+
[loading]="isCreating()"
|
|
1679
|
+
(onClick)="createBoard()"
|
|
1680
|
+
/>
|
|
1681
|
+
</div>
|
|
1682
|
+
</ng-template>
|
|
1683
|
+
</p-dialog>
|
|
1684
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i2$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i9.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i1$1.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "directive", type: i4.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i6$1.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "directive", type: i5.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["pTextareaPT", "pTextareaUnstyled", "autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "directive", type: i2.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "pipe", type: i9$1.DatePipe, name: "date" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
|
|
1685
|
+
}
|
|
1686
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BoardListPageComponent, decorators: [{
|
|
1687
|
+
type: Component,
|
|
1688
|
+
args: [{
|
|
1689
|
+
selector: 'tm-board-list-page',
|
|
1690
|
+
imports: [AngularModule, PrimeModule, TranslatePipe],
|
|
1691
|
+
template: `
|
|
1692
|
+
<div class="p-2 sm:p-4">
|
|
1693
|
+
<div
|
|
1694
|
+
class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-4"
|
|
1695
|
+
>
|
|
1696
|
+
<div>
|
|
1697
|
+
<h1 class="text-xl sm:text-2xl font-bold m-0">
|
|
1698
|
+
{{ 'task.manager.boards.title' | translate }}
|
|
1699
|
+
</h1>
|
|
1700
|
+
<p class="text-muted-color mt-1 text-sm sm:text-base">
|
|
1701
|
+
{{ 'task.manager.boards.subtitle' | translate }}
|
|
1702
|
+
</p>
|
|
1703
|
+
</div>
|
|
1704
|
+
<p-button
|
|
1705
|
+
[label]="'task.manager.board.new' | translate"
|
|
1706
|
+
icon="pi pi-plus"
|
|
1707
|
+
(onClick)="showCreateDialog.set(true)"
|
|
1708
|
+
/>
|
|
1709
|
+
</div>
|
|
1710
|
+
|
|
1711
|
+
@if (state.isLoading()) {
|
|
1712
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
1713
|
+
@for (i of [1, 2, 3, 4, 5, 6]; track i) {
|
|
1714
|
+
<div class="card !p-0 overflow-hidden">
|
|
1715
|
+
<p-skeleton height="4px" borderRadius="0" />
|
|
1716
|
+
<div class="p-5">
|
|
1717
|
+
<p-skeleton height="18px" width="60%" class="mb-3" />
|
|
1718
|
+
<p-skeleton height="13px" class="mb-1" />
|
|
1719
|
+
<p-skeleton height="13px" width="70%" class="mb-5" />
|
|
1720
|
+
<div class="flex gap-5 pt-4 border-t border-surface-100">
|
|
1721
|
+
<p-skeleton height="14px" width="55px" />
|
|
1722
|
+
<p-skeleton height="14px" width="45px" />
|
|
1723
|
+
<p-skeleton height="14px" width="50px" />
|
|
1724
|
+
</div>
|
|
1725
|
+
<p-skeleton height="11px" width="100px" class="mt-2" />
|
|
1726
|
+
</div>
|
|
1727
|
+
</div>
|
|
1728
|
+
}
|
|
1729
|
+
</div>
|
|
1730
|
+
} @else if (state.boards().length === 0) {
|
|
1731
|
+
<div
|
|
1732
|
+
class="flex flex-col items-center justify-center py-20 text-muted-color"
|
|
1733
|
+
>
|
|
1734
|
+
<span
|
|
1735
|
+
class="w-16 h-16 rounded-2xl border-2 border-dashed border-surface-300 flex items-center justify-center mb-4"
|
|
1736
|
+
>
|
|
1737
|
+
<i class="pi pi-th-large text-2xl text-surface-400"></i>
|
|
1738
|
+
</span>
|
|
1739
|
+
<p class="text-base font-semibold mb-1">
|
|
1740
|
+
{{ 'task.manager.board.empty' | translate }}
|
|
1741
|
+
</p>
|
|
1742
|
+
<p class="text-sm text-muted-color mb-5">
|
|
1743
|
+
{{ 'task.manager.board.empty.hint' | translate }}
|
|
1744
|
+
</p>
|
|
1745
|
+
<p-button
|
|
1746
|
+
[label]="'task.manager.board.new' | translate"
|
|
1747
|
+
icon="pi pi-plus"
|
|
1748
|
+
size="small"
|
|
1749
|
+
(onClick)="showCreateDialog.set(true)"
|
|
1750
|
+
/>
|
|
1751
|
+
</div>
|
|
1752
|
+
} @else {
|
|
1753
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
1754
|
+
@for (board of state.boards(); track board.id) {
|
|
1755
|
+
<div
|
|
1756
|
+
class="card !p-0 cursor-pointer overflow-hidden group hover:shadow-lg transition-all duration-200"
|
|
1757
|
+
(click)="openBoard(board.id)"
|
|
1758
|
+
>
|
|
1759
|
+
<div class="h-1" [style.background-color]="board.color"></div>
|
|
1760
|
+
|
|
1761
|
+
<div class="p-5">
|
|
1762
|
+
<div class="flex items-start justify-between gap-3 mb-2">
|
|
1763
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
1764
|
+
<h3
|
|
1765
|
+
class="font-semibold text-base m-0 truncate group-hover:text-primary-500 transition-colors duration-150"
|
|
1766
|
+
>
|
|
1767
|
+
{{ board.name }}
|
|
1768
|
+
</h3>
|
|
1769
|
+
</div>
|
|
1770
|
+
<i
|
|
1771
|
+
class="pi pi-arrow-right text-xs text-surface-300 group-hover:text-primary-500 group-hover:translate-x-0.5 transition-all duration-150 flex-shrink-0 mt-1"
|
|
1772
|
+
></i>
|
|
1773
|
+
</div>
|
|
1774
|
+
|
|
1775
|
+
<p
|
|
1776
|
+
class="text-sm text-muted-color line-clamp-2 pl-[18px] mb-0 min-h-[40px]"
|
|
1777
|
+
>
|
|
1778
|
+
{{ board.description || '' }}
|
|
1779
|
+
</p>
|
|
1780
|
+
|
|
1781
|
+
<div
|
|
1782
|
+
class="border-t border-surface-100 dark:border-surface-700 mt-4 pt-4 space-y-2.5"
|
|
1783
|
+
>
|
|
1784
|
+
<div class="flex items-center gap-4">
|
|
1785
|
+
<span class="flex items-center gap-1.5">
|
|
1786
|
+
<i class="pi pi-users text-[11px] text-muted-color"></i>
|
|
1787
|
+
<span class="text-sm font-semibold">{{
|
|
1788
|
+
board.totalMembers ?? 0
|
|
1789
|
+
}}</span>
|
|
1790
|
+
<span class="text-xs text-muted-color">{{
|
|
1791
|
+
'task.manager.board.members' | translate
|
|
1792
|
+
}}</span>
|
|
1793
|
+
</span>
|
|
1794
|
+
<span
|
|
1795
|
+
class="w-px h-3 bg-surface-200 dark:bg-surface-600"
|
|
1796
|
+
></span>
|
|
1797
|
+
<span class="flex items-center gap-1.5">
|
|
1798
|
+
<i class="pi pi-list text-[11px] text-muted-color"></i>
|
|
1799
|
+
<span class="text-sm font-semibold">{{
|
|
1800
|
+
board.totalLists ?? 0
|
|
1801
|
+
}}</span>
|
|
1802
|
+
<span class="text-xs text-muted-color">{{
|
|
1803
|
+
'task.manager.board.lists' | translate
|
|
1804
|
+
}}</span>
|
|
1805
|
+
</span>
|
|
1806
|
+
<span
|
|
1807
|
+
class="w-px h-3 bg-surface-200 dark:bg-surface-600"
|
|
1808
|
+
></span>
|
|
1809
|
+
<span class="flex items-center gap-1.5">
|
|
1810
|
+
<i
|
|
1811
|
+
class="pi pi-check-square text-[11px] text-muted-color"
|
|
1812
|
+
></i>
|
|
1813
|
+
<span class="text-sm font-semibold">{{
|
|
1814
|
+
board.totalTasks ?? 0
|
|
1815
|
+
}}</span>
|
|
1816
|
+
<span class="text-xs text-muted-color">{{
|
|
1817
|
+
'task.manager.board.tasks' | translate
|
|
1818
|
+
}}</span>
|
|
1819
|
+
</span>
|
|
1820
|
+
</div>
|
|
1821
|
+
<div
|
|
1822
|
+
class="flex items-center gap-1.5 text-xs text-muted-color"
|
|
1823
|
+
>
|
|
1824
|
+
<i class="pi pi-clock text-[10px]"></i>
|
|
1825
|
+
<span>{{ board.createdAt | date: 'MMM d, yyyy' }}</span>
|
|
1826
|
+
</div>
|
|
1827
|
+
</div>
|
|
1828
|
+
</div>
|
|
1829
|
+
</div>
|
|
1830
|
+
}
|
|
1831
|
+
</div>
|
|
1832
|
+
}
|
|
1833
|
+
</div>
|
|
1834
|
+
|
|
1835
|
+
<p-dialog
|
|
1836
|
+
[header]="'task.manager.board.new' | translate"
|
|
1837
|
+
[(visible)]="showCreateDialog"
|
|
1838
|
+
[modal]="true"
|
|
1839
|
+
[style]="{ width: '440px' }"
|
|
1840
|
+
>
|
|
1841
|
+
<div class="flex flex-col gap-4 py-2">
|
|
1842
|
+
<div class="flex flex-col gap-1">
|
|
1843
|
+
<label class="text-sm font-medium"
|
|
1844
|
+
>{{ 'shared.name' | translate }}
|
|
1845
|
+
<span class="text-red-500">*</span></label
|
|
1846
|
+
>
|
|
1847
|
+
<input
|
|
1848
|
+
pInputText
|
|
1849
|
+
[(ngModel)]="newBoardName"
|
|
1850
|
+
[placeholder]="'task.manager.board.name.placeholder' | translate"
|
|
1851
|
+
/>
|
|
1852
|
+
</div>
|
|
1853
|
+
<div class="flex flex-col gap-1">
|
|
1854
|
+
<label class="text-sm font-medium">{{
|
|
1855
|
+
'shared.description' | translate
|
|
1856
|
+
}}</label>
|
|
1857
|
+
<textarea
|
|
1858
|
+
pTextarea
|
|
1859
|
+
[(ngModel)]="newBoardDescription"
|
|
1860
|
+
rows="2"
|
|
1861
|
+
[placeholder]="
|
|
1862
|
+
'task.manager.board.description.placeholder' | translate
|
|
1863
|
+
"
|
|
1864
|
+
></textarea>
|
|
1865
|
+
</div>
|
|
1866
|
+
<div class="flex flex-col gap-2">
|
|
1867
|
+
<label class="text-sm font-medium">{{
|
|
1868
|
+
'task.manager.board.color.label' | translate
|
|
1869
|
+
}}</label>
|
|
1870
|
+
<div class="flex flex-wrap gap-2">
|
|
1871
|
+
@for (c of boardColors; track c) {
|
|
1872
|
+
<button
|
|
1873
|
+
type="button"
|
|
1874
|
+
class="w-8 h-8 rounded-full border-2 transition-all hover:scale-110 focus:outline-none flex items-center justify-center"
|
|
1875
|
+
[style.background-color]="c"
|
|
1876
|
+
[class.ring-2]="newBoardColor === c"
|
|
1877
|
+
[class.ring-offset-2]="newBoardColor === c"
|
|
1878
|
+
[class.ring-surface-900]="newBoardColor === c"
|
|
1879
|
+
[pTooltip]="c"
|
|
1880
|
+
(click)="newBoardColor = c"
|
|
1881
|
+
>
|
|
1882
|
+
@if (newBoardColor === c) {
|
|
1883
|
+
<i class="pi pi-check text-white text-xs"></i>
|
|
1884
|
+
}
|
|
1885
|
+
</button>
|
|
1886
|
+
}
|
|
1887
|
+
</div>
|
|
1888
|
+
<div class="flex items-center gap-2 mt-1">
|
|
1889
|
+
<span
|
|
1890
|
+
class="w-8 h-8 rounded-full border border-surface-200 flex-shrink-0"
|
|
1891
|
+
[style.background-color]="newBoardColor"
|
|
1892
|
+
></span>
|
|
1893
|
+
<span class="text-sm text-surface-500 font-mono">{{
|
|
1894
|
+
newBoardColor
|
|
1895
|
+
}}</span>
|
|
1896
|
+
</div>
|
|
1897
|
+
</div>
|
|
1898
|
+
</div>
|
|
1899
|
+
|
|
1900
|
+
<ng-template pTemplate="footer">
|
|
1901
|
+
<div class="flex justify-end gap-2">
|
|
1902
|
+
<p-button
|
|
1903
|
+
[label]="'shared.cancel' | translate"
|
|
1904
|
+
severity="secondary"
|
|
1905
|
+
(onClick)="closeCreateDialog()"
|
|
1906
|
+
/>
|
|
1907
|
+
<p-button
|
|
1908
|
+
[label]="'shared.create' | translate"
|
|
1909
|
+
[loading]="isCreating()"
|
|
1910
|
+
(onClick)="createBoard()"
|
|
1911
|
+
/>
|
|
1912
|
+
</div>
|
|
1913
|
+
</ng-template>
|
|
1914
|
+
</p-dialog>
|
|
1915
|
+
`,
|
|
1916
|
+
}]
|
|
1917
|
+
}], ctorParameters: () => [] });
|
|
1918
|
+
|
|
1919
|
+
var boardListPage_component = /*#__PURE__*/Object.freeze({
|
|
1920
|
+
__proto__: null,
|
|
1921
|
+
BoardListPageComponent: BoardListPageComponent
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
class BoardViewPageComponent {
|
|
1925
|
+
state = inject(TaskBoardStateService);
|
|
1926
|
+
taskApi = inject(TaskApiService);
|
|
1927
|
+
listApi = inject(TaskListApiService);
|
|
1928
|
+
confirmationService = inject(ConfirmationService);
|
|
1929
|
+
messageService = inject(MessageService);
|
|
1930
|
+
translateAdapter = inject(TranslateService, { optional: true });
|
|
1931
|
+
destroyRef = inject(DestroyRef);
|
|
1932
|
+
translate(key, vars) {
|
|
1933
|
+
return this.translateAdapter?.translate(key, vars) ?? key;
|
|
1934
|
+
}
|
|
1935
|
+
boardId = input.required(...(ngDevMode ? [{ debugName: "boardId" }] : []));
|
|
1936
|
+
viewMode = signal('board', ...(ngDevMode ? [{ debugName: "viewMode" }] : []));
|
|
1937
|
+
showTaskForm = signal(false, ...(ngDevMode ? [{ debugName: "showTaskForm" }] : []));
|
|
1938
|
+
editingTask = signal(null, ...(ngDevMode ? [{ debugName: "editingTask" }] : []));
|
|
1939
|
+
activeListId = signal('', ...(ngDevMode ? [{ debugName: "activeListId" }] : []));
|
|
1940
|
+
showAddListDialog = signal(false, ...(ngDevMode ? [{ debugName: "showAddListDialog" }] : []));
|
|
1941
|
+
isAddingList = signal(false, ...(ngDevMode ? [{ debugName: "isAddingList" }] : []));
|
|
1942
|
+
showLabelsDialog = signal(false, ...(ngDevMode ? [{ debugName: "showLabelsDialog" }] : []));
|
|
1943
|
+
isAddingLabel = signal(false, ...(ngDevMode ? [{ debugName: "isAddingLabel" }] : []));
|
|
1944
|
+
showMembersDialog = signal(false, ...(ngDevMode ? [{ debugName: "showMembersDialog" }] : []));
|
|
1945
|
+
isAddingMember = signal(false, ...(ngDevMode ? [{ debugName: "isAddingMember" }] : []));
|
|
1946
|
+
editingListId = signal(null, ...(ngDevMode ? [{ debugName: "editingListId" }] : []));
|
|
1947
|
+
editingListNameValue = signal('', ...(ngDevMode ? [{ debugName: "editingListNameValue" }] : []));
|
|
1948
|
+
showEditBoardDialog = signal(false, ...(ngDevMode ? [{ debugName: "showEditBoardDialog" }] : []));
|
|
1949
|
+
isSavingBoard = signal(false, ...(ngDevMode ? [{ debugName: "isSavingBoard" }] : []));
|
|
1950
|
+
newListName = '';
|
|
1951
|
+
newLabelName = '';
|
|
1952
|
+
newLabelColor = '#6366F1';
|
|
1953
|
+
selectedMemberUser = null;
|
|
1954
|
+
newMemberRole = 'MEMBER';
|
|
1955
|
+
editBoardName = '';
|
|
1956
|
+
editBoardDescription = '';
|
|
1957
|
+
editBoardColor = '#6366F1';
|
|
1958
|
+
boardColorPalette = BOARD_COLOR_PALETTE;
|
|
1959
|
+
memberRoleOptions = [
|
|
1960
|
+
{ labelKey: 'task.manager.member.role.admin', value: 'ADMIN' },
|
|
1961
|
+
{ labelKey: 'task.manager.member.role.member', value: 'MEMBER' },
|
|
1962
|
+
{ labelKey: 'task.manager.member.role.viewer', value: 'VIEWER' },
|
|
1963
|
+
];
|
|
1964
|
+
allTasksFlat = computed(() => this.state.listsWithTasks().flatMap((list) => list.tasks.map((task) => ({
|
|
1965
|
+
...task,
|
|
1966
|
+
listName: list.name,
|
|
1967
|
+
listColor: list.color,
|
|
1968
|
+
}))), ...(ngDevMode ? [{ debugName: "allTasksFlat" }] : []));
|
|
1969
|
+
listDropIds = computed(() => this.state.listsWithTasks().map((l) => l.id), ...(ngDevMode ? [{ debugName: "listDropIds" }] : []));
|
|
1970
|
+
constructor() {
|
|
1971
|
+
effect(() => { const id = this.boardId(); untracked(() => this.state.openBoard(id)); });
|
|
1972
|
+
this.destroyRef.onDestroy(() => this.state.closeBoard());
|
|
1973
|
+
}
|
|
1974
|
+
openAddTask(list) {
|
|
1975
|
+
this.editingTask.set(null);
|
|
1976
|
+
this.activeListId.set(list.id);
|
|
1977
|
+
this.showTaskForm.set(true);
|
|
1978
|
+
}
|
|
1979
|
+
onTaskSaved(task) {
|
|
1980
|
+
this.state.addTaskOptimistic(task);
|
|
1981
|
+
}
|
|
1982
|
+
onDeleteTask(task) {
|
|
1983
|
+
this.confirmationService.confirm({
|
|
1984
|
+
message: this.translate('shared.confirm.delete.item', { name: task.title }),
|
|
1985
|
+
header: this.translate('task.detail.delete'),
|
|
1986
|
+
icon: 'pi pi-exclamation-triangle',
|
|
1987
|
+
acceptButtonStyleClass: 'p-button-danger',
|
|
1988
|
+
accept: async () => {
|
|
1989
|
+
await this.state.deleteTask(task.id);
|
|
1990
|
+
this.messageService.add({
|
|
1991
|
+
severity: 'success',
|
|
1992
|
+
summary: this.translate('shared.success'),
|
|
1993
|
+
detail: this.translate('task.delete.success'),
|
|
1994
|
+
});
|
|
1995
|
+
},
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
onEditTask(task) {
|
|
1999
|
+
this.editingTask.set(task);
|
|
2000
|
+
this.activeListId.set(task.listId);
|
|
2001
|
+
this.showTaskForm.set(true);
|
|
2002
|
+
this.state.selectTask(null);
|
|
2003
|
+
}
|
|
2004
|
+
getMemberName(assigneeId) {
|
|
2005
|
+
return this.state.members().find((m) => m.userId === assigneeId)?.userName ?? null;
|
|
2006
|
+
}
|
|
2007
|
+
getMemberInitials(assigneeId) {
|
|
2008
|
+
return memberInitials(this.getMemberName(assigneeId));
|
|
2009
|
+
}
|
|
2010
|
+
memberInitials(member) {
|
|
2011
|
+
return memberInitials(member.userName);
|
|
2012
|
+
}
|
|
2013
|
+
memberColor(member) {
|
|
2014
|
+
return memberColorFromId(member.userId);
|
|
2015
|
+
}
|
|
2016
|
+
memberColorById(userId) {
|
|
2017
|
+
return memberColorFromId(userId);
|
|
2018
|
+
}
|
|
2019
|
+
roleBadgeClass(role) {
|
|
2020
|
+
const map = {
|
|
2021
|
+
OWNER: 'text-primary-600 dark:text-primary-400 font-semibold',
|
|
2022
|
+
ADMIN: 'text-orange-600 dark:text-orange-400 font-semibold',
|
|
2023
|
+
MEMBER: 'text-surface-500 dark:text-surface-400',
|
|
2024
|
+
VIEWER: 'text-surface-400 dark:text-surface-500',
|
|
2025
|
+
};
|
|
2026
|
+
return map[role] ?? map['MEMBER'];
|
|
2027
|
+
}
|
|
2028
|
+
prioritySeverity = taskPrioritySeverity;
|
|
2029
|
+
isOverdue = isTaskOverdue;
|
|
2030
|
+
async onTaskDrop(event) {
|
|
2031
|
+
if (event.previousContainer === event.container) {
|
|
2032
|
+
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
|
2033
|
+
}
|
|
2034
|
+
else {
|
|
2035
|
+
transferArrayItem(event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex);
|
|
2036
|
+
const task = event.container.data[event.currentIndex];
|
|
2037
|
+
await this.taskApi.moveTask({
|
|
2038
|
+
taskId: task.id,
|
|
2039
|
+
targetListId: event.container.id,
|
|
2040
|
+
position: event.currentIndex * 100,
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
async onListDrop(event) {
|
|
2045
|
+
if (event.previousIndex === event.currentIndex)
|
|
2046
|
+
return;
|
|
2047
|
+
const lists = this.state.listsWithTasks().slice();
|
|
2048
|
+
moveItemInArray(lists, event.previousIndex, event.currentIndex);
|
|
2049
|
+
const orderedIds = lists.map((l) => l.id);
|
|
2050
|
+
await this.state.reorderLists(this.boardId(), orderedIds);
|
|
2051
|
+
}
|
|
2052
|
+
startRenameList(list) {
|
|
2053
|
+
this.editingListId.set(list.id);
|
|
2054
|
+
this.editingListNameValue.set(list.name);
|
|
2055
|
+
}
|
|
2056
|
+
cancelRenameList() {
|
|
2057
|
+
this.editingListId.set(null);
|
|
2058
|
+
this.editingListNameValue.set('');
|
|
2059
|
+
}
|
|
2060
|
+
async saveRenameList() {
|
|
2061
|
+
const id = this.editingListId();
|
|
2062
|
+
const name = this.editingListNameValue().trim();
|
|
2063
|
+
this.cancelRenameList();
|
|
2064
|
+
if (!id || !name)
|
|
2065
|
+
return;
|
|
2066
|
+
await this.state.renameList(id, name);
|
|
2067
|
+
this.messageService.add({
|
|
2068
|
+
severity: 'success',
|
|
2069
|
+
summary: this.translate('shared.success'),
|
|
2070
|
+
detail: this.translate('task.list.update.success'),
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
openEditBoardDialog(board) {
|
|
2074
|
+
this.editBoardName = board.name;
|
|
2075
|
+
this.editBoardDescription = board.description ?? '';
|
|
2076
|
+
this.editBoardColor = board.color;
|
|
2077
|
+
this.showEditBoardDialog.set(true);
|
|
2078
|
+
}
|
|
2079
|
+
async saveBoard() {
|
|
2080
|
+
const board = this.state.activeBoard();
|
|
2081
|
+
if (!board || !this.editBoardName.trim())
|
|
2082
|
+
return;
|
|
2083
|
+
this.isSavingBoard.set(true);
|
|
2084
|
+
try {
|
|
2085
|
+
await this.state.updateBoard({
|
|
2086
|
+
id: board.id,
|
|
2087
|
+
name: this.editBoardName.trim(),
|
|
2088
|
+
description: this.editBoardDescription.trim() || undefined,
|
|
2089
|
+
color: this.editBoardColor,
|
|
2090
|
+
});
|
|
2091
|
+
this.showEditBoardDialog.set(false);
|
|
2092
|
+
this.messageService.add({
|
|
2093
|
+
severity: 'success',
|
|
2094
|
+
summary: this.translate('shared.success'),
|
|
2095
|
+
detail: this.translate('task.board.update.success'),
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
finally {
|
|
2099
|
+
this.isSavingBoard.set(false);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
async addLabel() {
|
|
2103
|
+
if (!this.newLabelName.trim())
|
|
2104
|
+
return;
|
|
2105
|
+
this.isAddingLabel.set(true);
|
|
2106
|
+
try {
|
|
2107
|
+
await this.state.addLabel(this.boardId(), this.newLabelName.trim(), this.newLabelColor);
|
|
2108
|
+
this.newLabelName = '';
|
|
2109
|
+
this.newLabelColor = '#6366F1';
|
|
2110
|
+
}
|
|
2111
|
+
finally {
|
|
2112
|
+
this.isAddingLabel.set(false);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
async removeLabel(labelId) {
|
|
2116
|
+
await this.state.deleteLabel(labelId);
|
|
2117
|
+
}
|
|
2118
|
+
async addMember() {
|
|
2119
|
+
if (!this.selectedMemberUser)
|
|
2120
|
+
return;
|
|
2121
|
+
this.isAddingMember.set(true);
|
|
2122
|
+
try {
|
|
2123
|
+
await this.state.addMember(this.selectedMemberUser, this.newMemberRole);
|
|
2124
|
+
this.resetMemberForm();
|
|
2125
|
+
}
|
|
2126
|
+
finally {
|
|
2127
|
+
this.isAddingMember.set(false);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
resetMemberForm() {
|
|
2131
|
+
this.selectedMemberUser = null;
|
|
2132
|
+
this.newMemberRole = 'MEMBER';
|
|
2133
|
+
}
|
|
2134
|
+
async removeMember(userId) {
|
|
2135
|
+
await this.state.removeMember(userId);
|
|
2136
|
+
}
|
|
2137
|
+
async addList() {
|
|
2138
|
+
if (!this.newListName.trim())
|
|
2139
|
+
return;
|
|
2140
|
+
this.isAddingList.set(true);
|
|
2141
|
+
try {
|
|
2142
|
+
const position = this.state.lists().length * 100;
|
|
2143
|
+
await this.listApi.insert({
|
|
2144
|
+
boardId: this.boardId(),
|
|
2145
|
+
name: this.newListName,
|
|
2146
|
+
position,
|
|
2147
|
+
});
|
|
2148
|
+
await this.state.openBoard(this.boardId());
|
|
2149
|
+
this.showAddListDialog.set(false);
|
|
2150
|
+
this.newListName = '';
|
|
2151
|
+
}
|
|
2152
|
+
finally {
|
|
2153
|
+
this.isAddingList.set(false);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BoardViewPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2157
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: BoardViewPageComponent, isStandalone: true, selector: "tm-board-view-page", inputs: { boardId: { classPropertyName: "boardId", publicName: "boardId", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
|
|
2158
|
+
<div class="flex flex-col h-full">
|
|
2159
|
+
<div class="flex flex-col">
|
|
2160
|
+
<div class="flex items-center justify-between px-5 py-2.5 gap-4">
|
|
2161
|
+
@if (state.activeBoard(); as board) {
|
|
2162
|
+
<div class="flex items-center gap-2.5 min-w-0 flex-1 group">
|
|
2163
|
+
<div class="flex flex-col min-w-0">
|
|
2164
|
+
<div class="flex items-center gap-1.5 min-w-0">
|
|
2165
|
+
<h1 class="text-base font-bold text-surface-900 dark:text-surface-0 truncate leading-snug">
|
|
2166
|
+
{{ board.name }}
|
|
2167
|
+
</h1>
|
|
2168
|
+
<p-button
|
|
2169
|
+
icon="pi pi-pencil"
|
|
2170
|
+
[text]="true"
|
|
2171
|
+
size="small"
|
|
2172
|
+
severity="secondary"
|
|
2173
|
+
styleClass="!w-6 !h-6 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
2174
|
+
[pTooltip]="'task.manager.board.edit' | translate"
|
|
2175
|
+
tooltipPosition="bottom"
|
|
2176
|
+
(onClick)="openEditBoardDialog(board)"
|
|
2177
|
+
/>
|
|
2178
|
+
</div>
|
|
2179
|
+
@if (board.description) {
|
|
2180
|
+
<span class="text-xs text-surface-400 truncate max-w-sm hidden md:block leading-snug">
|
|
2181
|
+
{{ board.description }}
|
|
2182
|
+
</span>
|
|
2183
|
+
}
|
|
2184
|
+
</div>
|
|
2185
|
+
</div>
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
<div class="flex items-center gap-1.5 flex-shrink-0">
|
|
2189
|
+
@if (state.members().length > 0) {
|
|
2190
|
+
<div
|
|
2191
|
+
class="flex items-center -space-x-2 cursor-pointer mr-1"
|
|
2192
|
+
(click)="showMembersDialog.set(true)"
|
|
2193
|
+
[pTooltip]="'task.manager.board.manage.members' | translate"
|
|
2194
|
+
tooltipPosition="bottom"
|
|
2195
|
+
>
|
|
2196
|
+
@for (member of state.members().slice(0, 4); track member.id) {
|
|
2197
|
+
<div
|
|
2198
|
+
class="w-6 h-6 rounded-full border-2 border-white dark:border-surface-900 flex items-center justify-center text-[9px] font-bold text-white flex-shrink-0 ring-1 ring-surface-100 dark:ring-surface-800"
|
|
2199
|
+
[style.background-color]="memberColor(member)"
|
|
2200
|
+
>
|
|
2201
|
+
{{ memberInitials(member) }}
|
|
2202
|
+
</div>
|
|
2203
|
+
}
|
|
2204
|
+
@if (state.members().length > 4) {
|
|
2205
|
+
<div
|
|
2206
|
+
class="w-6 h-6 rounded-full border-2 border-white dark:border-surface-900 bg-surface-200 dark:bg-surface-700 flex items-center justify-center text-[9px] font-bold text-surface-600 dark:text-surface-300 flex-shrink-0"
|
|
2207
|
+
>
|
|
2208
|
+
+{{ state.members().length - 4 }}
|
|
2209
|
+
</div>
|
|
2210
|
+
}
|
|
2211
|
+
</div>
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
<div class="h-5 w-px bg-surface-200 dark:bg-surface-700 mx-0.5"></div>
|
|
2215
|
+
|
|
2216
|
+
<div class="flex rounded-lg overflow-hidden border border-surface-200 dark:border-surface-700">
|
|
2217
|
+
<p-button
|
|
2218
|
+
icon="pi pi-th-large"
|
|
2219
|
+
[pTooltip]="'task.manager.view.board' | translate"
|
|
2220
|
+
tooltipPosition="bottom"
|
|
2221
|
+
size="small"
|
|
2222
|
+
[severity]="viewMode() === 'board' ? 'primary' : 'secondary'"
|
|
2223
|
+
[outlined]="viewMode() !== 'board'"
|
|
2224
|
+
styleClass="!rounded-none !border-0"
|
|
2225
|
+
(onClick)="viewMode.set('board')"
|
|
2226
|
+
/>
|
|
2227
|
+
<p-button
|
|
2228
|
+
icon="pi pi-list"
|
|
2229
|
+
[pTooltip]="'task.manager.view.list' | translate"
|
|
2230
|
+
tooltipPosition="bottom"
|
|
2231
|
+
size="small"
|
|
2232
|
+
[severity]="viewMode() === 'list' ? 'primary' : 'secondary'"
|
|
2233
|
+
[outlined]="viewMode() !== 'list'"
|
|
2234
|
+
styleClass="!rounded-none !border-0"
|
|
2235
|
+
(onClick)="viewMode.set('list')"
|
|
2236
|
+
/>
|
|
2237
|
+
</div>
|
|
2238
|
+
|
|
2239
|
+
<div class="h-5 w-px bg-surface-200 dark:bg-surface-700 mx-0.5"></div>
|
|
2240
|
+
|
|
2241
|
+
<p-button
|
|
2242
|
+
icon="pi pi-users"
|
|
2243
|
+
[text]="true"
|
|
2244
|
+
severity="secondary"
|
|
2245
|
+
size="small"
|
|
2246
|
+
styleClass="!w-8 !h-8"
|
|
2247
|
+
[pTooltip]="'task.manager.board.manage.members' | translate"
|
|
2248
|
+
tooltipPosition="bottom"
|
|
2249
|
+
(onClick)="showMembersDialog.set(true)"
|
|
2250
|
+
/>
|
|
2251
|
+
<p-button
|
|
2252
|
+
icon="pi pi-tag"
|
|
2253
|
+
[text]="true"
|
|
2254
|
+
severity="secondary"
|
|
2255
|
+
size="small"
|
|
2256
|
+
styleClass="!w-8 !h-8"
|
|
2257
|
+
[pTooltip]="'task.manager.board.manage.labels' | translate"
|
|
2258
|
+
tooltipPosition="bottom"
|
|
2259
|
+
(onClick)="showLabelsDialog.set(true)"
|
|
2260
|
+
/>
|
|
2261
|
+
|
|
2262
|
+
@if (viewMode() === 'board') {
|
|
2263
|
+
<div class="h-5 w-px bg-surface-200 dark:bg-surface-700 mx-0.5"></div>
|
|
2264
|
+
<p-button
|
|
2265
|
+
icon="pi pi-plus"
|
|
2266
|
+
[label]="'task.manager.board.add.list' | translate"
|
|
2267
|
+
size="small"
|
|
2268
|
+
(onClick)="showAddListDialog.set(true)"
|
|
2269
|
+
/>
|
|
2270
|
+
}
|
|
2271
|
+
</div>
|
|
2272
|
+
</div>
|
|
2273
|
+
|
|
2274
|
+
@if (state.activeBoard(); as board) {
|
|
2275
|
+
<div class="h-0.5 w-full flex-shrink-0" [style.background-color]="board.color"></div>
|
|
2276
|
+
}
|
|
2277
|
+
</div>
|
|
2278
|
+
|
|
2279
|
+
@if (state.isBoardLoading()) {
|
|
2280
|
+
@if (viewMode() === 'board') {
|
|
2281
|
+
<div class="flex gap-4 p-6 overflow-x-auto">
|
|
2282
|
+
@for (i of [1, 2, 3]; track i) {
|
|
2283
|
+
<div class="flex-shrink-0 w-72">
|
|
2284
|
+
<p-skeleton height="36px" class="mb-3" />
|
|
2285
|
+
@for (j of [1, 2, 3]; track j) {
|
|
2286
|
+
<p-skeleton height="90px" class="mb-2" />
|
|
2287
|
+
}
|
|
2288
|
+
</div>
|
|
2289
|
+
}
|
|
2290
|
+
</div>
|
|
2291
|
+
} @else {
|
|
2292
|
+
<div class="p-6">
|
|
2293
|
+
@for (i of [1, 2, 3, 4, 5]; track i) {
|
|
2294
|
+
<p-skeleton height="48px" class="mb-2" />
|
|
2295
|
+
}
|
|
2296
|
+
</div>
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
} @else if (viewMode() === 'board') {
|
|
2300
|
+
<div
|
|
2301
|
+
class="flex gap-4 p-5 overflow-x-auto flex-1"
|
|
2302
|
+
cdkDropList
|
|
2303
|
+
cdkDropListOrientation="horizontal"
|
|
2304
|
+
[cdkDropListData]="state.listsWithTasks()"
|
|
2305
|
+
(cdkDropListDropped)="onListDrop($event)"
|
|
2306
|
+
>
|
|
2307
|
+
@for (list of state.listsWithTasks(); track list.id) {
|
|
2308
|
+
<div
|
|
2309
|
+
cdkDrag
|
|
2310
|
+
class="flex-shrink-0 w-72 flex flex-col max-h-[calc(100vh-140px)] self-start"
|
|
2311
|
+
>
|
|
2312
|
+
<div
|
|
2313
|
+
class="flex items-center gap-1 px-2 py-2.5 rounded-t-xl border border-b-0 border-surface-200 dark:border-surface-700"
|
|
2314
|
+
[style.border-top]="'3px solid ' + list.color"
|
|
2315
|
+
>
|
|
2316
|
+
<i
|
|
2317
|
+
cdkDragHandle
|
|
2318
|
+
class="pi pi-bars text-surface-300 dark:text-surface-600 text-xs cursor-grab flex-shrink-0 px-1"
|
|
2319
|
+
></i>
|
|
2320
|
+
|
|
2321
|
+
<div class="flex items-center gap-2 min-w-0 flex-1">
|
|
2322
|
+
@if (editingListId() === list.id) {
|
|
2323
|
+
<input
|
|
2324
|
+
pInputText
|
|
2325
|
+
class="text-sm font-semibold !h-6 !py-0 !px-1 w-full"
|
|
2326
|
+
[ngModel]="editingListNameValue()"
|
|
2327
|
+
(ngModelChange)="editingListNameValue.set($event)"
|
|
2328
|
+
(keydown.enter)="saveRenameList()"
|
|
2329
|
+
(keydown.escape)="cancelRenameList()"
|
|
2330
|
+
(blur)="cancelRenameList()"
|
|
2331
|
+
(click)="$event.stopPropagation()"
|
|
2332
|
+
autofocus
|
|
2333
|
+
/>
|
|
2334
|
+
} @else {
|
|
2335
|
+
<span class="font-semibold text-sm truncate">{{ list.name }}</span>
|
|
2336
|
+
<span
|
|
2337
|
+
class="text-[11px] font-semibold px-1.5 py-0.5 rounded-full text-white flex-shrink-0"
|
|
2338
|
+
[style.background-color]="list.color"
|
|
2339
|
+
>
|
|
2340
|
+
{{ list.tasks.length }}
|
|
2341
|
+
</span>
|
|
2342
|
+
}
|
|
2343
|
+
</div>
|
|
2344
|
+
|
|
2345
|
+
@if (editingListId() === list.id) {
|
|
2346
|
+
<p-button
|
|
2347
|
+
icon="pi pi-check"
|
|
2348
|
+
[text]="true"
|
|
2349
|
+
size="small"
|
|
2350
|
+
severity="success"
|
|
2351
|
+
styleClass="!w-6 !h-6 flex-shrink-0"
|
|
2352
|
+
(mousedown)="$event.preventDefault()"
|
|
2353
|
+
(click)="saveRenameList(); $event.stopPropagation()"
|
|
2354
|
+
/>
|
|
2355
|
+
<p-button
|
|
2356
|
+
icon="pi pi-times"
|
|
2357
|
+
[text]="true"
|
|
2358
|
+
size="small"
|
|
2359
|
+
severity="secondary"
|
|
2360
|
+
styleClass="!w-6 !h-6 flex-shrink-0"
|
|
2361
|
+
(mousedown)="$event.preventDefault()"
|
|
2362
|
+
(click)="cancelRenameList(); $event.stopPropagation()"
|
|
2363
|
+
/>
|
|
2364
|
+
} @else {
|
|
2365
|
+
<p-button
|
|
2366
|
+
icon="pi pi-pencil"
|
|
2367
|
+
[text]="true"
|
|
2368
|
+
size="small"
|
|
2369
|
+
severity="secondary"
|
|
2370
|
+
styleClass="!w-6 !h-6 flex-shrink-0"
|
|
2371
|
+
[pTooltip]="'task.manager.list.rename' | translate"
|
|
2372
|
+
tooltipPosition="bottom"
|
|
2373
|
+
(click)="startRenameList(list); $event.stopPropagation()"
|
|
2374
|
+
/>
|
|
2375
|
+
}
|
|
2376
|
+
</div>
|
|
2377
|
+
|
|
2378
|
+
<div
|
|
2379
|
+
cdkDropList
|
|
2380
|
+
[id]="list.id"
|
|
2381
|
+
[cdkDropListData]="list.tasks"
|
|
2382
|
+
[cdkDropListConnectedTo]="listDropIds()"
|
|
2383
|
+
(cdkDropListDropped)="onTaskDrop($event)"
|
|
2384
|
+
class="flex flex-col gap-2 p-2 overflow-y-auto flex-1 min-h-[60px] border border-t-0 border-b-0 border-surface-200 dark:border-surface-700"
|
|
2385
|
+
>
|
|
2386
|
+
@for (task of list.tasks; track task.id) {
|
|
2387
|
+
<div cdkDrag>
|
|
2388
|
+
<tm-task-card
|
|
2389
|
+
[task]="task"
|
|
2390
|
+
(cardClick)="state.selectTask($event)"
|
|
2391
|
+
(editClick)="onEditTask($event)"
|
|
2392
|
+
(deleteClick)="onDeleteTask($event)"
|
|
2393
|
+
/>
|
|
2394
|
+
</div>
|
|
2395
|
+
}
|
|
2396
|
+
@if (list.tasks.length === 0) {
|
|
2397
|
+
<div
|
|
2398
|
+
class="flex items-center justify-center h-14 border-2 border-dashed border-surface-200 dark:border-surface-700 rounded-lg"
|
|
2399
|
+
>
|
|
2400
|
+
<span class="text-xs text-surface-400">{{
|
|
2401
|
+
'task.manager.board.drop.tasks.here' | translate
|
|
2402
|
+
}}</span>
|
|
2403
|
+
</div>
|
|
2404
|
+
}
|
|
2405
|
+
</div>
|
|
2406
|
+
|
|
2407
|
+
<div
|
|
2408
|
+
class="rounded-b-xl border border-t-0 border-surface-200 dark:border-surface-700 p-1.5"
|
|
2409
|
+
>
|
|
2410
|
+
<p-button
|
|
2411
|
+
icon="pi pi-plus"
|
|
2412
|
+
[label]="'task.manager.board.add.task' | translate"
|
|
2413
|
+
severity="secondary"
|
|
2414
|
+
size="small"
|
|
2415
|
+
styleClass="w-full !justify-start"
|
|
2416
|
+
(onClick)="openAddTask(list)"
|
|
2417
|
+
/>
|
|
2418
|
+
</div>
|
|
2419
|
+
</div>
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
@if (state.listsWithTasks().length === 0) {
|
|
2423
|
+
<div class="flex-1 flex items-center justify-center">
|
|
2424
|
+
<div class="text-center text-surface-400">
|
|
2425
|
+
<div
|
|
2426
|
+
class="w-16 h-16 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
|
2427
|
+
>
|
|
2428
|
+
<i
|
|
2429
|
+
class="pi pi-list text-3xl text-surface-300 dark:text-surface-600"
|
|
2430
|
+
></i>
|
|
2431
|
+
</div>
|
|
2432
|
+
<p class="font-medium text-surface-500">
|
|
2433
|
+
{{ 'task.manager.board.list.empty' | translate }}
|
|
2434
|
+
</p>
|
|
2435
|
+
<p-button
|
|
2436
|
+
icon="pi pi-plus"
|
|
2437
|
+
[label]="'task.manager.board.add.list' | translate"
|
|
2438
|
+
severity="secondary"
|
|
2439
|
+
size="small"
|
|
2440
|
+
class="mt-3"
|
|
2441
|
+
(onClick)="showAddListDialog.set(true)"
|
|
2442
|
+
/>
|
|
2443
|
+
</div>
|
|
2444
|
+
</div>
|
|
2445
|
+
}
|
|
2446
|
+
</div>
|
|
2447
|
+
|
|
2448
|
+
} @else {
|
|
2449
|
+
<div class="p-4 sm:p-6 flex-1 overflow-auto">
|
|
2450
|
+
<div class="card">
|
|
2451
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
2452
|
+
<p-table
|
|
2453
|
+
[value]="allTasksFlat()"
|
|
2454
|
+
[paginator]="allTasksFlat().length > 20"
|
|
2455
|
+
[rows]="20"
|
|
2456
|
+
[rowsPerPageOptions]="[10, 20, 50]"
|
|
2457
|
+
[scrollable]="true"
|
|
2458
|
+
class="p-datatable-sm"
|
|
2459
|
+
[tableStyle]="{ 'min-width': '700px' }"
|
|
2460
|
+
>
|
|
2461
|
+
<ng-template #header>
|
|
2462
|
+
<tr>
|
|
2463
|
+
<th pSortableColumn="title" class="min-w-[240px]">
|
|
2464
|
+
{{ 'task.manager.table.title' | translate }}
|
|
2465
|
+
<p-sortIcon field="title" />
|
|
2466
|
+
</th>
|
|
2467
|
+
<th pSortableColumn="listName" class="w-40">
|
|
2468
|
+
{{ 'task.manager.table.list' | translate }}
|
|
2469
|
+
<p-sortIcon field="listName" />
|
|
2470
|
+
</th>
|
|
2471
|
+
<th pSortableColumn="priority" class="w-32">
|
|
2472
|
+
{{ 'task.manager.table.priority' | translate }}
|
|
2473
|
+
<p-sortIcon field="priority" />
|
|
2474
|
+
</th>
|
|
2475
|
+
<th class="w-36">
|
|
2476
|
+
{{ 'task.manager.table.assignee' | translate }}
|
|
2477
|
+
</th>
|
|
2478
|
+
<th pSortableColumn="dueDate" class="w-32">
|
|
2479
|
+
{{ 'task.manager.table.due.date' | translate }}
|
|
2480
|
+
<p-sortIcon field="dueDate" />
|
|
2481
|
+
</th>
|
|
2482
|
+
<th class="w-[100px]">{{ 'shared.actions' | translate }}</th>
|
|
2483
|
+
</tr>
|
|
2484
|
+
</ng-template>
|
|
2485
|
+
<ng-template #body let-task>
|
|
2486
|
+
<tr>
|
|
2487
|
+
<td>
|
|
2488
|
+
<span
|
|
2489
|
+
class="font-medium text-sm cursor-pointer hover:text-primary-500 transition-colors"
|
|
2490
|
+
(click)="state.selectTask(task)"
|
|
2491
|
+
>{{ task.title }}</span>
|
|
2492
|
+
</td>
|
|
2493
|
+
<td>
|
|
2494
|
+
<span class="inline-flex items-center gap-1.5 text-sm">
|
|
2495
|
+
<span
|
|
2496
|
+
class="w-2 h-2 rounded-full flex-shrink-0"
|
|
2497
|
+
[style.background-color]="task.listColor"
|
|
2498
|
+
></span>
|
|
2499
|
+
{{ task.listName }}
|
|
2500
|
+
</span>
|
|
2501
|
+
</td>
|
|
2502
|
+
<td>
|
|
2503
|
+
<p-tag
|
|
2504
|
+
[value]="task.priority"
|
|
2505
|
+
[severity]="prioritySeverity(task.priority)"
|
|
2506
|
+
/>
|
|
2507
|
+
</td>
|
|
2508
|
+
<td>
|
|
2509
|
+
@if (task.assignees?.length) {
|
|
2510
|
+
<div class="flex items-center -space-x-1.5">
|
|
2511
|
+
@for (a of task.assignees!.slice(0, 3); track a.userId) {
|
|
2512
|
+
<span
|
|
2513
|
+
class="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white flex-shrink-0 ring-2 ring-white dark:ring-surface-800"
|
|
2514
|
+
[style.background-color]="memberColorById(a.userId)"
|
|
2515
|
+
[pTooltip]="a.userName ?? ''"
|
|
2516
|
+
>
|
|
2517
|
+
{{ getMemberInitials(a.userId) }}
|
|
2518
|
+
</span>
|
|
2519
|
+
}
|
|
2520
|
+
@if (task.assignees!.length > 3) {
|
|
2521
|
+
<span
|
|
2522
|
+
class="w-6 h-6 rounded-full bg-surface-200 dark:bg-surface-700 flex items-center justify-center text-[10px] font-bold text-surface-600 ring-2 ring-white dark:ring-surface-800 flex-shrink-0"
|
|
2523
|
+
>
|
|
2524
|
+
+{{ task.assignees!.length - 3 }}
|
|
2525
|
+
</span>
|
|
2526
|
+
}
|
|
2527
|
+
</div>
|
|
2528
|
+
} @else {
|
|
2529
|
+
<span class="text-muted-color text-sm">—</span>
|
|
2530
|
+
}
|
|
2531
|
+
</td>
|
|
2532
|
+
<td>
|
|
2533
|
+
@if (task.dueDate) {
|
|
2534
|
+
<span
|
|
2535
|
+
class="text-sm"
|
|
2536
|
+
[class.text-red-500]="isOverdue(task.dueDate)"
|
|
2537
|
+
>
|
|
2538
|
+
{{ task.dueDate | date: 'MMM d, y' }}
|
|
2539
|
+
</span>
|
|
2540
|
+
} @else {
|
|
2541
|
+
<span class="text-muted-color text-sm">—</span>
|
|
2542
|
+
}
|
|
2543
|
+
</td>
|
|
2544
|
+
<td>
|
|
2545
|
+
<div class="flex gap-1">
|
|
2546
|
+
<p-button
|
|
2547
|
+
icon="pi pi-pencil"
|
|
2548
|
+
[text]="true"
|
|
2549
|
+
severity="secondary"
|
|
2550
|
+
size="small"
|
|
2551
|
+
[pTooltip]="'shared.edit' | translate"
|
|
2552
|
+
(onClick)="onEditTask(task)"
|
|
2553
|
+
/>
|
|
2554
|
+
<p-button
|
|
2555
|
+
icon="pi pi-trash"
|
|
2556
|
+
[text]="true"
|
|
2557
|
+
severity="danger"
|
|
2558
|
+
size="small"
|
|
2559
|
+
[pTooltip]="'shared.delete' | translate"
|
|
2560
|
+
(onClick)="onDeleteTask(task)"
|
|
2561
|
+
/>
|
|
2562
|
+
</div>
|
|
2563
|
+
</td>
|
|
2564
|
+
</tr>
|
|
2565
|
+
</ng-template>
|
|
2566
|
+
<ng-template #emptymessage>
|
|
2567
|
+
<tr>
|
|
2568
|
+
<td colspan="6" class="text-center text-muted-color py-8">
|
|
2569
|
+
<i class="pi pi-inbox text-4xl block mb-3"></i>
|
|
2570
|
+
{{ 'task.manager.table.empty' | translate }}
|
|
2571
|
+
</td>
|
|
2572
|
+
</tr>
|
|
2573
|
+
</ng-template>
|
|
2574
|
+
</p-table>
|
|
2575
|
+
</div>
|
|
2576
|
+
</div>
|
|
2577
|
+
</div>
|
|
2578
|
+
}
|
|
2579
|
+
</div>
|
|
2580
|
+
|
|
2581
|
+
<tm-task-form-dialog
|
|
2582
|
+
[(visible)]="showTaskForm"
|
|
2583
|
+
[task]="editingTask()"
|
|
2584
|
+
[boardId]="boardId()"
|
|
2585
|
+
[listId]="activeListId()"
|
|
2586
|
+
[members]="state.members()"
|
|
2587
|
+
[labels]="state.labels()"
|
|
2588
|
+
(saved)="onTaskSaved($event)"
|
|
2589
|
+
/>
|
|
2590
|
+
|
|
2591
|
+
<tm-task-detail-panel
|
|
2592
|
+
[task]="state.selectedTask()"
|
|
2593
|
+
(closePanel)="state.selectTask(null)"
|
|
2594
|
+
(editTask)="onEditTask($event)"
|
|
2595
|
+
/>
|
|
2596
|
+
|
|
2597
|
+
<p-dialog
|
|
2598
|
+
[header]="'task.manager.label.manage.title' | translate"
|
|
2599
|
+
[(visible)]="showLabelsDialog"
|
|
2600
|
+
[modal]="true"
|
|
2601
|
+
[style]="{ width: '420px' }"
|
|
2602
|
+
(onHide)="newLabelName = ''; newLabelColor = '#6366F1'"
|
|
2603
|
+
>
|
|
2604
|
+
<div class="flex flex-col gap-4 py-2">
|
|
2605
|
+
<div class="flex gap-2 items-end">
|
|
2606
|
+
<div class="flex flex-col gap-1 flex-1">
|
|
2607
|
+
<label class="text-sm font-medium">{{
|
|
2608
|
+
'task.manager.label.name.label' | translate
|
|
2609
|
+
}}</label>
|
|
2610
|
+
<input
|
|
2611
|
+
pInputText
|
|
2612
|
+
[(ngModel)]="newLabelName"
|
|
2613
|
+
[placeholder]="'task.manager.label.name.placeholder' | translate"
|
|
2614
|
+
/>
|
|
2615
|
+
</div>
|
|
2616
|
+
<div class="flex flex-col gap-1">
|
|
2617
|
+
<label class="text-sm font-medium">{{
|
|
2618
|
+
'task.manager.label.color.label' | translate
|
|
2619
|
+
}}</label>
|
|
2620
|
+
<input
|
|
2621
|
+
type="color"
|
|
2622
|
+
[(ngModel)]="newLabelColor"
|
|
2623
|
+
class="w-10 h-10 rounded cursor-pointer border border-surface-300"
|
|
2624
|
+
/>
|
|
2625
|
+
</div>
|
|
2626
|
+
<p-button
|
|
2627
|
+
icon="pi pi-plus"
|
|
2628
|
+
[label]="'task.manager.label.add' | translate"
|
|
2629
|
+
size="small"
|
|
2630
|
+
[loading]="isAddingLabel()"
|
|
2631
|
+
(onClick)="addLabel()"
|
|
2632
|
+
/>
|
|
2633
|
+
</div>
|
|
2634
|
+
|
|
2635
|
+
<div class="flex flex-col gap-2 max-h-64 overflow-y-auto">
|
|
2636
|
+
@if (state.labels().length === 0) {
|
|
2637
|
+
<p class="text-sm text-surface-400 text-center py-4">
|
|
2638
|
+
{{ 'task.manager.label.no.labels' | translate }}
|
|
2639
|
+
</p>
|
|
2640
|
+
}
|
|
2641
|
+
@for (label of state.labels(); track label.id) {
|
|
2642
|
+
<div
|
|
2643
|
+
class="flex items-center justify-between px-3 py-2 rounded-lg border border-surface-200 dark:border-surface-700"
|
|
2644
|
+
>
|
|
2645
|
+
<div class="flex items-center gap-2">
|
|
2646
|
+
<span
|
|
2647
|
+
class="w-4 h-4 rounded-full flex-shrink-0"
|
|
2648
|
+
[style.background-color]="label.color"
|
|
2649
|
+
></span>
|
|
2650
|
+
<span class="text-sm font-medium">{{ label.name }}</span>
|
|
2651
|
+
</div>
|
|
2652
|
+
<p-button
|
|
2653
|
+
icon="pi pi-trash"
|
|
2654
|
+
severity="danger"
|
|
2655
|
+
size="small"
|
|
2656
|
+
[text]="true"
|
|
2657
|
+
(onClick)="removeLabel(label.id)"
|
|
2658
|
+
/>
|
|
2659
|
+
</div>
|
|
2660
|
+
}
|
|
2661
|
+
</div>
|
|
2662
|
+
</div>
|
|
2663
|
+
</p-dialog>
|
|
2664
|
+
|
|
2665
|
+
<p-dialog
|
|
2666
|
+
[header]="'task.manager.member.manage.title' | translate"
|
|
2667
|
+
[(visible)]="showMembersDialog"
|
|
2668
|
+
[modal]="true"
|
|
2669
|
+
[style]="{ width: '480px' }"
|
|
2670
|
+
(onHide)="resetMemberForm()"
|
|
2671
|
+
>
|
|
2672
|
+
<div class="flex flex-col gap-4 py-2">
|
|
2673
|
+
<div class="flex flex-col gap-3">
|
|
2674
|
+
<div class="flex flex-col gap-1">
|
|
2675
|
+
<label class="text-sm font-medium">{{
|
|
2676
|
+
'task.manager.member.user.label' | translate
|
|
2677
|
+
}}</label>
|
|
2678
|
+
<lib-user-select
|
|
2679
|
+
[(value)]="selectedMemberUser"
|
|
2680
|
+
[isEditMode]="true"
|
|
2681
|
+
[placeHolder]="'task.manager.member.user.placeholder' | translate"
|
|
2682
|
+
/>
|
|
2683
|
+
</div>
|
|
2684
|
+
|
|
2685
|
+
<div class="flex gap-2 items-end">
|
|
2686
|
+
<div class="flex flex-col gap-1 flex-1">
|
|
2687
|
+
<label class="text-sm font-medium">{{
|
|
2688
|
+
'task.manager.member.role.label' | translate
|
|
2689
|
+
}}</label>
|
|
2690
|
+
<p-select
|
|
2691
|
+
[(ngModel)]="newMemberRole"
|
|
2692
|
+
[options]="memberRoleOptions"
|
|
2693
|
+
optionValue="value"
|
|
2694
|
+
[placeholder]="
|
|
2695
|
+
'task.manager.member.role.placeholder' | translate
|
|
2696
|
+
"
|
|
2697
|
+
class="w-full"
|
|
2698
|
+
appendTo="body"
|
|
2699
|
+
>
|
|
2700
|
+
<ng-template #selectedItem let-opt>
|
|
2701
|
+
{{ opt.labelKey | translate }}
|
|
2702
|
+
</ng-template>
|
|
2703
|
+
<ng-template #item let-opt>
|
|
2704
|
+
{{ opt.labelKey | translate }}
|
|
2705
|
+
</ng-template>
|
|
2706
|
+
</p-select>
|
|
2707
|
+
</div>
|
|
2708
|
+
<p-button
|
|
2709
|
+
icon="pi pi-plus"
|
|
2710
|
+
[label]="'task.manager.member.add' | translate"
|
|
2711
|
+
size="small"
|
|
2712
|
+
[loading]="isAddingMember()"
|
|
2713
|
+
[disabled]="!selectedMemberUser"
|
|
2714
|
+
(onClick)="addMember()"
|
|
2715
|
+
/>
|
|
2716
|
+
</div>
|
|
2717
|
+
</div>
|
|
2718
|
+
|
|
2719
|
+
<div class="flex flex-col gap-2 max-h-72 overflow-y-auto">
|
|
2720
|
+
@if (state.members().length === 0) {
|
|
2721
|
+
<p class="text-sm text-surface-400 text-center py-4">
|
|
2722
|
+
{{ 'task.manager.member.no.members' | translate }}
|
|
2723
|
+
</p>
|
|
2724
|
+
}
|
|
2725
|
+
@for (member of state.members(); track member.id) {
|
|
2726
|
+
<div
|
|
2727
|
+
class="flex items-center justify-between px-3 py-2.5 rounded-lg border border-surface-200 dark:border-surface-700"
|
|
2728
|
+
>
|
|
2729
|
+
<div class="flex items-center gap-3">
|
|
2730
|
+
<div
|
|
2731
|
+
class="w-8 h-8 rounded-full flex items-center justify-center text-[11px] font-bold text-white flex-shrink-0"
|
|
2732
|
+
[style.background-color]="memberColor(member)"
|
|
2733
|
+
>
|
|
2734
|
+
{{ memberInitials(member) }}
|
|
2735
|
+
</div>
|
|
2736
|
+
<div>
|
|
2737
|
+
<div class="text-sm font-medium ">
|
|
2738
|
+
{{ member.userName ?? member.userId }}
|
|
2739
|
+
</div>
|
|
2740
|
+
@if (member.userEmail) {
|
|
2741
|
+
<div class="text-xs text-surface-400">
|
|
2742
|
+
{{ member.userEmail }}
|
|
2743
|
+
</div>
|
|
2744
|
+
}
|
|
2745
|
+
</div>
|
|
2746
|
+
</div>
|
|
2747
|
+
<div class="flex items-center gap-2">
|
|
2748
|
+
<span
|
|
2749
|
+
class="text-[10px] font-semibold px-2 py-0.5 rounded-full"
|
|
2750
|
+
[class]="roleBadgeClass(member.role)"
|
|
2751
|
+
>
|
|
2752
|
+
{{ ('task.manager.member.role.' + member.role.toLowerCase()) | translate }}
|
|
2753
|
+
</span>
|
|
2754
|
+
@if (member.role !== 'OWNER') {
|
|
2755
|
+
<p-button
|
|
2756
|
+
icon="pi pi-trash"
|
|
2757
|
+
severity="danger"
|
|
2758
|
+
size="small"
|
|
2759
|
+
[text]="true"
|
|
2760
|
+
(onClick)="removeMember(member.userId)"
|
|
2761
|
+
/>
|
|
2762
|
+
}
|
|
2763
|
+
</div>
|
|
2764
|
+
</div>
|
|
2765
|
+
}
|
|
2766
|
+
</div>
|
|
2767
|
+
</div>
|
|
2768
|
+
</p-dialog>
|
|
2769
|
+
|
|
2770
|
+
<p-dialog
|
|
2771
|
+
[header]="'task.manager.list.new' | translate"
|
|
2772
|
+
[(visible)]="showAddListDialog"
|
|
2773
|
+
[modal]="true"
|
|
2774
|
+
[style]="{ width: '360px' }"
|
|
2775
|
+
(onHide)="newListName = ''"
|
|
2776
|
+
>
|
|
2777
|
+
<div class="flex flex-col gap-2 py-2">
|
|
2778
|
+
<label class="text-sm font-medium">
|
|
2779
|
+
{{ 'task.manager.list.name.label' | translate }}
|
|
2780
|
+
<span class="text-red-500">*</span>
|
|
2781
|
+
</label>
|
|
2782
|
+
<input
|
|
2783
|
+
pInputText
|
|
2784
|
+
[(ngModel)]="newListName"
|
|
2785
|
+
[placeholder]="'task.manager.list.name.placeholder' | translate"
|
|
2786
|
+
/>
|
|
2787
|
+
</div>
|
|
2788
|
+
<ng-template pTemplate="footer">
|
|
2789
|
+
<div class="flex justify-end gap-2">
|
|
2790
|
+
<p-button
|
|
2791
|
+
[label]="'shared.cancel' | translate"
|
|
2792
|
+
severity="secondary"
|
|
2793
|
+
(onClick)="showAddListDialog.set(false)"
|
|
2794
|
+
/>
|
|
2795
|
+
<p-button
|
|
2796
|
+
[label]="'shared.create' | translate"
|
|
2797
|
+
[loading]="isAddingList()"
|
|
2798
|
+
(onClick)="addList()"
|
|
2799
|
+
/>
|
|
2800
|
+
</div>
|
|
2801
|
+
</ng-template>
|
|
2802
|
+
</p-dialog>
|
|
2803
|
+
|
|
2804
|
+
<p-dialog
|
|
2805
|
+
[header]="'task.manager.board.edit' | translate"
|
|
2806
|
+
[(visible)]="showEditBoardDialog"
|
|
2807
|
+
[modal]="true"
|
|
2808
|
+
[style]="{ width: '420px' }"
|
|
2809
|
+
>
|
|
2810
|
+
<div class="flex flex-col gap-4 py-2">
|
|
2811
|
+
<div class="flex flex-col gap-1">
|
|
2812
|
+
<label class="text-sm font-medium">
|
|
2813
|
+
{{ 'task.manager.board.name.label' | translate }}
|
|
2814
|
+
<span class="text-red-500">*</span>
|
|
2815
|
+
</label>
|
|
2816
|
+
<input pInputText [(ngModel)]="editBoardName" />
|
|
2817
|
+
</div>
|
|
2818
|
+
<div class="flex flex-col gap-1">
|
|
2819
|
+
<label class="text-sm font-medium">{{
|
|
2820
|
+
'task.manager.board.description.label' | translate
|
|
2821
|
+
}}</label>
|
|
2822
|
+
<textarea
|
|
2823
|
+
pTextarea
|
|
2824
|
+
[(ngModel)]="editBoardDescription"
|
|
2825
|
+
[rows]="3"
|
|
2826
|
+
[placeholder]="'task.manager.board.description.placeholder' | translate"
|
|
2827
|
+
></textarea>
|
|
2828
|
+
</div>
|
|
2829
|
+
<div class="flex flex-col gap-1">
|
|
2830
|
+
<label class="text-sm font-medium">{{
|
|
2831
|
+
'task.manager.board.color.label' | translate
|
|
2832
|
+
}}</label>
|
|
2833
|
+
<div class="flex items-center gap-3">
|
|
2834
|
+
<input
|
|
2835
|
+
type="color"
|
|
2836
|
+
[(ngModel)]="editBoardColor"
|
|
2837
|
+
class="w-10 h-10 rounded cursor-pointer border border-surface-300"
|
|
2838
|
+
/>
|
|
2839
|
+
<div class="flex gap-2 flex-wrap">
|
|
2840
|
+
@for (color of boardColorPalette; track color) {
|
|
2841
|
+
<button
|
|
2842
|
+
type="button"
|
|
2843
|
+
class="w-6 h-6 rounded-full border-2 transition-transform hover:scale-110"
|
|
2844
|
+
[style.background-color]="color"
|
|
2845
|
+
[class.border-surface-900]="editBoardColor === color"
|
|
2846
|
+
[class.border-transparent]="editBoardColor !== color"
|
|
2847
|
+
(click)="editBoardColor = color"
|
|
2848
|
+
></button>
|
|
2849
|
+
}
|
|
2850
|
+
</div>
|
|
2851
|
+
</div>
|
|
2852
|
+
</div>
|
|
2853
|
+
</div>
|
|
2854
|
+
<ng-template pTemplate="footer">
|
|
2855
|
+
<div class="flex justify-end gap-2">
|
|
2856
|
+
<p-button
|
|
2857
|
+
[label]="'shared.cancel' | translate"
|
|
2858
|
+
severity="secondary"
|
|
2859
|
+
(onClick)="showEditBoardDialog.set(false)"
|
|
2860
|
+
/>
|
|
2861
|
+
<p-button
|
|
2862
|
+
[label]="'shared.save' | translate"
|
|
2863
|
+
[loading]="isSavingBoard()"
|
|
2864
|
+
[disabled]="!editBoardName.trim()"
|
|
2865
|
+
(onClick)="saveBoard()"
|
|
2866
|
+
/>
|
|
2867
|
+
</div>
|
|
2868
|
+
</ng-template>
|
|
2869
|
+
</p-dialog>
|
|
2870
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$3.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$3.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i1$3.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i2$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i9.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i1$1.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "directive", type: i4.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i6.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "component", type: i6$1.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "component", type: i9$2.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i9$2.SortableColumn, selector: "[pSortableColumn]", inputs: ["pSortableColumn", "pSortableColumnDisabled"] }, { kind: "component", type: i9$2.SortIcon, selector: "p-sortIcon", inputs: ["field"] }, { kind: "component", type: i1.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "directive", type: i5.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["pTextareaPT", "pTextareaUnstyled", "autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "directive", type: i2.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "component", type: TaskCardComponent, selector: "tm-task-card", inputs: ["task"], outputs: ["cardClick", "editClick", "deleteClick"] }, { kind: "component", type: TaskFormDialogComponent, selector: "tm-task-form-dialog", inputs: ["visible", "task", "boardId", "listId", "members", "labels"], outputs: ["visibleChange", "saved"] }, { kind: "component", type: TaskDetailPanelComponent, selector: "tm-task-detail-panel", inputs: ["task"], outputs: ["closePanel", "editTask"] }, { kind: "component", type: UserSelectComponent, selector: "lib-user-select", inputs: ["showClear", "value"], outputs: ["valueChange", "userSelected"] }, { kind: "pipe", type: i9$1.DatePipe, name: "date" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
|
|
2871
|
+
}
|
|
2872
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BoardViewPageComponent, decorators: [{
|
|
2873
|
+
type: Component,
|
|
2874
|
+
args: [{
|
|
2875
|
+
selector: 'tm-board-view-page',
|
|
2876
|
+
imports: [
|
|
2877
|
+
DragDropModule,
|
|
2878
|
+
AngularModule,
|
|
2879
|
+
PrimeModule,
|
|
2880
|
+
TaskCardComponent,
|
|
2881
|
+
TaskFormDialogComponent,
|
|
2882
|
+
TaskDetailPanelComponent,
|
|
2883
|
+
TranslatePipe,
|
|
2884
|
+
UserSelectComponent,
|
|
2885
|
+
],
|
|
2886
|
+
template: `
|
|
2887
|
+
<div class="flex flex-col h-full">
|
|
2888
|
+
<div class="flex flex-col">
|
|
2889
|
+
<div class="flex items-center justify-between px-5 py-2.5 gap-4">
|
|
2890
|
+
@if (state.activeBoard(); as board) {
|
|
2891
|
+
<div class="flex items-center gap-2.5 min-w-0 flex-1 group">
|
|
2892
|
+
<div class="flex flex-col min-w-0">
|
|
2893
|
+
<div class="flex items-center gap-1.5 min-w-0">
|
|
2894
|
+
<h1 class="text-base font-bold text-surface-900 dark:text-surface-0 truncate leading-snug">
|
|
2895
|
+
{{ board.name }}
|
|
2896
|
+
</h1>
|
|
2897
|
+
<p-button
|
|
2898
|
+
icon="pi pi-pencil"
|
|
2899
|
+
[text]="true"
|
|
2900
|
+
size="small"
|
|
2901
|
+
severity="secondary"
|
|
2902
|
+
styleClass="!w-6 !h-6 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
2903
|
+
[pTooltip]="'task.manager.board.edit' | translate"
|
|
2904
|
+
tooltipPosition="bottom"
|
|
2905
|
+
(onClick)="openEditBoardDialog(board)"
|
|
2906
|
+
/>
|
|
2907
|
+
</div>
|
|
2908
|
+
@if (board.description) {
|
|
2909
|
+
<span class="text-xs text-surface-400 truncate max-w-sm hidden md:block leading-snug">
|
|
2910
|
+
{{ board.description }}
|
|
2911
|
+
</span>
|
|
2912
|
+
}
|
|
2913
|
+
</div>
|
|
2914
|
+
</div>
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
<div class="flex items-center gap-1.5 flex-shrink-0">
|
|
2918
|
+
@if (state.members().length > 0) {
|
|
2919
|
+
<div
|
|
2920
|
+
class="flex items-center -space-x-2 cursor-pointer mr-1"
|
|
2921
|
+
(click)="showMembersDialog.set(true)"
|
|
2922
|
+
[pTooltip]="'task.manager.board.manage.members' | translate"
|
|
2923
|
+
tooltipPosition="bottom"
|
|
2924
|
+
>
|
|
2925
|
+
@for (member of state.members().slice(0, 4); track member.id) {
|
|
2926
|
+
<div
|
|
2927
|
+
class="w-6 h-6 rounded-full border-2 border-white dark:border-surface-900 flex items-center justify-center text-[9px] font-bold text-white flex-shrink-0 ring-1 ring-surface-100 dark:ring-surface-800"
|
|
2928
|
+
[style.background-color]="memberColor(member)"
|
|
2929
|
+
>
|
|
2930
|
+
{{ memberInitials(member) }}
|
|
2931
|
+
</div>
|
|
2932
|
+
}
|
|
2933
|
+
@if (state.members().length > 4) {
|
|
2934
|
+
<div
|
|
2935
|
+
class="w-6 h-6 rounded-full border-2 border-white dark:border-surface-900 bg-surface-200 dark:bg-surface-700 flex items-center justify-center text-[9px] font-bold text-surface-600 dark:text-surface-300 flex-shrink-0"
|
|
2936
|
+
>
|
|
2937
|
+
+{{ state.members().length - 4 }}
|
|
2938
|
+
</div>
|
|
2939
|
+
}
|
|
2940
|
+
</div>
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
<div class="h-5 w-px bg-surface-200 dark:bg-surface-700 mx-0.5"></div>
|
|
2944
|
+
|
|
2945
|
+
<div class="flex rounded-lg overflow-hidden border border-surface-200 dark:border-surface-700">
|
|
2946
|
+
<p-button
|
|
2947
|
+
icon="pi pi-th-large"
|
|
2948
|
+
[pTooltip]="'task.manager.view.board' | translate"
|
|
2949
|
+
tooltipPosition="bottom"
|
|
2950
|
+
size="small"
|
|
2951
|
+
[severity]="viewMode() === 'board' ? 'primary' : 'secondary'"
|
|
2952
|
+
[outlined]="viewMode() !== 'board'"
|
|
2953
|
+
styleClass="!rounded-none !border-0"
|
|
2954
|
+
(onClick)="viewMode.set('board')"
|
|
2955
|
+
/>
|
|
2956
|
+
<p-button
|
|
2957
|
+
icon="pi pi-list"
|
|
2958
|
+
[pTooltip]="'task.manager.view.list' | translate"
|
|
2959
|
+
tooltipPosition="bottom"
|
|
2960
|
+
size="small"
|
|
2961
|
+
[severity]="viewMode() === 'list' ? 'primary' : 'secondary'"
|
|
2962
|
+
[outlined]="viewMode() !== 'list'"
|
|
2963
|
+
styleClass="!rounded-none !border-0"
|
|
2964
|
+
(onClick)="viewMode.set('list')"
|
|
2965
|
+
/>
|
|
2966
|
+
</div>
|
|
2967
|
+
|
|
2968
|
+
<div class="h-5 w-px bg-surface-200 dark:bg-surface-700 mx-0.5"></div>
|
|
2969
|
+
|
|
2970
|
+
<p-button
|
|
2971
|
+
icon="pi pi-users"
|
|
2972
|
+
[text]="true"
|
|
2973
|
+
severity="secondary"
|
|
2974
|
+
size="small"
|
|
2975
|
+
styleClass="!w-8 !h-8"
|
|
2976
|
+
[pTooltip]="'task.manager.board.manage.members' | translate"
|
|
2977
|
+
tooltipPosition="bottom"
|
|
2978
|
+
(onClick)="showMembersDialog.set(true)"
|
|
2979
|
+
/>
|
|
2980
|
+
<p-button
|
|
2981
|
+
icon="pi pi-tag"
|
|
2982
|
+
[text]="true"
|
|
2983
|
+
severity="secondary"
|
|
2984
|
+
size="small"
|
|
2985
|
+
styleClass="!w-8 !h-8"
|
|
2986
|
+
[pTooltip]="'task.manager.board.manage.labels' | translate"
|
|
2987
|
+
tooltipPosition="bottom"
|
|
2988
|
+
(onClick)="showLabelsDialog.set(true)"
|
|
2989
|
+
/>
|
|
2990
|
+
|
|
2991
|
+
@if (viewMode() === 'board') {
|
|
2992
|
+
<div class="h-5 w-px bg-surface-200 dark:bg-surface-700 mx-0.5"></div>
|
|
2993
|
+
<p-button
|
|
2994
|
+
icon="pi pi-plus"
|
|
2995
|
+
[label]="'task.manager.board.add.list' | translate"
|
|
2996
|
+
size="small"
|
|
2997
|
+
(onClick)="showAddListDialog.set(true)"
|
|
2998
|
+
/>
|
|
2999
|
+
}
|
|
3000
|
+
</div>
|
|
3001
|
+
</div>
|
|
3002
|
+
|
|
3003
|
+
@if (state.activeBoard(); as board) {
|
|
3004
|
+
<div class="h-0.5 w-full flex-shrink-0" [style.background-color]="board.color"></div>
|
|
3005
|
+
}
|
|
3006
|
+
</div>
|
|
3007
|
+
|
|
3008
|
+
@if (state.isBoardLoading()) {
|
|
3009
|
+
@if (viewMode() === 'board') {
|
|
3010
|
+
<div class="flex gap-4 p-6 overflow-x-auto">
|
|
3011
|
+
@for (i of [1, 2, 3]; track i) {
|
|
3012
|
+
<div class="flex-shrink-0 w-72">
|
|
3013
|
+
<p-skeleton height="36px" class="mb-3" />
|
|
3014
|
+
@for (j of [1, 2, 3]; track j) {
|
|
3015
|
+
<p-skeleton height="90px" class="mb-2" />
|
|
3016
|
+
}
|
|
3017
|
+
</div>
|
|
3018
|
+
}
|
|
3019
|
+
</div>
|
|
3020
|
+
} @else {
|
|
3021
|
+
<div class="p-6">
|
|
3022
|
+
@for (i of [1, 2, 3, 4, 5]; track i) {
|
|
3023
|
+
<p-skeleton height="48px" class="mb-2" />
|
|
3024
|
+
}
|
|
3025
|
+
</div>
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
} @else if (viewMode() === 'board') {
|
|
3029
|
+
<div
|
|
3030
|
+
class="flex gap-4 p-5 overflow-x-auto flex-1"
|
|
3031
|
+
cdkDropList
|
|
3032
|
+
cdkDropListOrientation="horizontal"
|
|
3033
|
+
[cdkDropListData]="state.listsWithTasks()"
|
|
3034
|
+
(cdkDropListDropped)="onListDrop($event)"
|
|
3035
|
+
>
|
|
3036
|
+
@for (list of state.listsWithTasks(); track list.id) {
|
|
3037
|
+
<div
|
|
3038
|
+
cdkDrag
|
|
3039
|
+
class="flex-shrink-0 w-72 flex flex-col max-h-[calc(100vh-140px)] self-start"
|
|
3040
|
+
>
|
|
3041
|
+
<div
|
|
3042
|
+
class="flex items-center gap-1 px-2 py-2.5 rounded-t-xl border border-b-0 border-surface-200 dark:border-surface-700"
|
|
3043
|
+
[style.border-top]="'3px solid ' + list.color"
|
|
3044
|
+
>
|
|
3045
|
+
<i
|
|
3046
|
+
cdkDragHandle
|
|
3047
|
+
class="pi pi-bars text-surface-300 dark:text-surface-600 text-xs cursor-grab flex-shrink-0 px-1"
|
|
3048
|
+
></i>
|
|
3049
|
+
|
|
3050
|
+
<div class="flex items-center gap-2 min-w-0 flex-1">
|
|
3051
|
+
@if (editingListId() === list.id) {
|
|
3052
|
+
<input
|
|
3053
|
+
pInputText
|
|
3054
|
+
class="text-sm font-semibold !h-6 !py-0 !px-1 w-full"
|
|
3055
|
+
[ngModel]="editingListNameValue()"
|
|
3056
|
+
(ngModelChange)="editingListNameValue.set($event)"
|
|
3057
|
+
(keydown.enter)="saveRenameList()"
|
|
3058
|
+
(keydown.escape)="cancelRenameList()"
|
|
3059
|
+
(blur)="cancelRenameList()"
|
|
3060
|
+
(click)="$event.stopPropagation()"
|
|
3061
|
+
autofocus
|
|
3062
|
+
/>
|
|
3063
|
+
} @else {
|
|
3064
|
+
<span class="font-semibold text-sm truncate">{{ list.name }}</span>
|
|
3065
|
+
<span
|
|
3066
|
+
class="text-[11px] font-semibold px-1.5 py-0.5 rounded-full text-white flex-shrink-0"
|
|
3067
|
+
[style.background-color]="list.color"
|
|
3068
|
+
>
|
|
3069
|
+
{{ list.tasks.length }}
|
|
3070
|
+
</span>
|
|
3071
|
+
}
|
|
3072
|
+
</div>
|
|
3073
|
+
|
|
3074
|
+
@if (editingListId() === list.id) {
|
|
3075
|
+
<p-button
|
|
3076
|
+
icon="pi pi-check"
|
|
3077
|
+
[text]="true"
|
|
3078
|
+
size="small"
|
|
3079
|
+
severity="success"
|
|
3080
|
+
styleClass="!w-6 !h-6 flex-shrink-0"
|
|
3081
|
+
(mousedown)="$event.preventDefault()"
|
|
3082
|
+
(click)="saveRenameList(); $event.stopPropagation()"
|
|
3083
|
+
/>
|
|
3084
|
+
<p-button
|
|
3085
|
+
icon="pi pi-times"
|
|
3086
|
+
[text]="true"
|
|
3087
|
+
size="small"
|
|
3088
|
+
severity="secondary"
|
|
3089
|
+
styleClass="!w-6 !h-6 flex-shrink-0"
|
|
3090
|
+
(mousedown)="$event.preventDefault()"
|
|
3091
|
+
(click)="cancelRenameList(); $event.stopPropagation()"
|
|
3092
|
+
/>
|
|
3093
|
+
} @else {
|
|
3094
|
+
<p-button
|
|
3095
|
+
icon="pi pi-pencil"
|
|
3096
|
+
[text]="true"
|
|
3097
|
+
size="small"
|
|
3098
|
+
severity="secondary"
|
|
3099
|
+
styleClass="!w-6 !h-6 flex-shrink-0"
|
|
3100
|
+
[pTooltip]="'task.manager.list.rename' | translate"
|
|
3101
|
+
tooltipPosition="bottom"
|
|
3102
|
+
(click)="startRenameList(list); $event.stopPropagation()"
|
|
3103
|
+
/>
|
|
3104
|
+
}
|
|
3105
|
+
</div>
|
|
3106
|
+
|
|
3107
|
+
<div
|
|
3108
|
+
cdkDropList
|
|
3109
|
+
[id]="list.id"
|
|
3110
|
+
[cdkDropListData]="list.tasks"
|
|
3111
|
+
[cdkDropListConnectedTo]="listDropIds()"
|
|
3112
|
+
(cdkDropListDropped)="onTaskDrop($event)"
|
|
3113
|
+
class="flex flex-col gap-2 p-2 overflow-y-auto flex-1 min-h-[60px] border border-t-0 border-b-0 border-surface-200 dark:border-surface-700"
|
|
3114
|
+
>
|
|
3115
|
+
@for (task of list.tasks; track task.id) {
|
|
3116
|
+
<div cdkDrag>
|
|
3117
|
+
<tm-task-card
|
|
3118
|
+
[task]="task"
|
|
3119
|
+
(cardClick)="state.selectTask($event)"
|
|
3120
|
+
(editClick)="onEditTask($event)"
|
|
3121
|
+
(deleteClick)="onDeleteTask($event)"
|
|
3122
|
+
/>
|
|
3123
|
+
</div>
|
|
3124
|
+
}
|
|
3125
|
+
@if (list.tasks.length === 0) {
|
|
3126
|
+
<div
|
|
3127
|
+
class="flex items-center justify-center h-14 border-2 border-dashed border-surface-200 dark:border-surface-700 rounded-lg"
|
|
3128
|
+
>
|
|
3129
|
+
<span class="text-xs text-surface-400">{{
|
|
3130
|
+
'task.manager.board.drop.tasks.here' | translate
|
|
3131
|
+
}}</span>
|
|
3132
|
+
</div>
|
|
3133
|
+
}
|
|
3134
|
+
</div>
|
|
3135
|
+
|
|
3136
|
+
<div
|
|
3137
|
+
class="rounded-b-xl border border-t-0 border-surface-200 dark:border-surface-700 p-1.5"
|
|
3138
|
+
>
|
|
3139
|
+
<p-button
|
|
3140
|
+
icon="pi pi-plus"
|
|
3141
|
+
[label]="'task.manager.board.add.task' | translate"
|
|
3142
|
+
severity="secondary"
|
|
3143
|
+
size="small"
|
|
3144
|
+
styleClass="w-full !justify-start"
|
|
3145
|
+
(onClick)="openAddTask(list)"
|
|
3146
|
+
/>
|
|
3147
|
+
</div>
|
|
3148
|
+
</div>
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
@if (state.listsWithTasks().length === 0) {
|
|
3152
|
+
<div class="flex-1 flex items-center justify-center">
|
|
3153
|
+
<div class="text-center text-surface-400">
|
|
3154
|
+
<div
|
|
3155
|
+
class="w-16 h-16 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
|
3156
|
+
>
|
|
3157
|
+
<i
|
|
3158
|
+
class="pi pi-list text-3xl text-surface-300 dark:text-surface-600"
|
|
3159
|
+
></i>
|
|
3160
|
+
</div>
|
|
3161
|
+
<p class="font-medium text-surface-500">
|
|
3162
|
+
{{ 'task.manager.board.list.empty' | translate }}
|
|
3163
|
+
</p>
|
|
3164
|
+
<p-button
|
|
3165
|
+
icon="pi pi-plus"
|
|
3166
|
+
[label]="'task.manager.board.add.list' | translate"
|
|
3167
|
+
severity="secondary"
|
|
3168
|
+
size="small"
|
|
3169
|
+
class="mt-3"
|
|
3170
|
+
(onClick)="showAddListDialog.set(true)"
|
|
3171
|
+
/>
|
|
3172
|
+
</div>
|
|
3173
|
+
</div>
|
|
3174
|
+
}
|
|
3175
|
+
</div>
|
|
3176
|
+
|
|
3177
|
+
} @else {
|
|
3178
|
+
<div class="p-4 sm:p-6 flex-1 overflow-auto">
|
|
3179
|
+
<div class="card">
|
|
3180
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
3181
|
+
<p-table
|
|
3182
|
+
[value]="allTasksFlat()"
|
|
3183
|
+
[paginator]="allTasksFlat().length > 20"
|
|
3184
|
+
[rows]="20"
|
|
3185
|
+
[rowsPerPageOptions]="[10, 20, 50]"
|
|
3186
|
+
[scrollable]="true"
|
|
3187
|
+
class="p-datatable-sm"
|
|
3188
|
+
[tableStyle]="{ 'min-width': '700px' }"
|
|
3189
|
+
>
|
|
3190
|
+
<ng-template #header>
|
|
3191
|
+
<tr>
|
|
3192
|
+
<th pSortableColumn="title" class="min-w-[240px]">
|
|
3193
|
+
{{ 'task.manager.table.title' | translate }}
|
|
3194
|
+
<p-sortIcon field="title" />
|
|
3195
|
+
</th>
|
|
3196
|
+
<th pSortableColumn="listName" class="w-40">
|
|
3197
|
+
{{ 'task.manager.table.list' | translate }}
|
|
3198
|
+
<p-sortIcon field="listName" />
|
|
3199
|
+
</th>
|
|
3200
|
+
<th pSortableColumn="priority" class="w-32">
|
|
3201
|
+
{{ 'task.manager.table.priority' | translate }}
|
|
3202
|
+
<p-sortIcon field="priority" />
|
|
3203
|
+
</th>
|
|
3204
|
+
<th class="w-36">
|
|
3205
|
+
{{ 'task.manager.table.assignee' | translate }}
|
|
3206
|
+
</th>
|
|
3207
|
+
<th pSortableColumn="dueDate" class="w-32">
|
|
3208
|
+
{{ 'task.manager.table.due.date' | translate }}
|
|
3209
|
+
<p-sortIcon field="dueDate" />
|
|
3210
|
+
</th>
|
|
3211
|
+
<th class="w-[100px]">{{ 'shared.actions' | translate }}</th>
|
|
3212
|
+
</tr>
|
|
3213
|
+
</ng-template>
|
|
3214
|
+
<ng-template #body let-task>
|
|
3215
|
+
<tr>
|
|
3216
|
+
<td>
|
|
3217
|
+
<span
|
|
3218
|
+
class="font-medium text-sm cursor-pointer hover:text-primary-500 transition-colors"
|
|
3219
|
+
(click)="state.selectTask(task)"
|
|
3220
|
+
>{{ task.title }}</span>
|
|
3221
|
+
</td>
|
|
3222
|
+
<td>
|
|
3223
|
+
<span class="inline-flex items-center gap-1.5 text-sm">
|
|
3224
|
+
<span
|
|
3225
|
+
class="w-2 h-2 rounded-full flex-shrink-0"
|
|
3226
|
+
[style.background-color]="task.listColor"
|
|
3227
|
+
></span>
|
|
3228
|
+
{{ task.listName }}
|
|
3229
|
+
</span>
|
|
3230
|
+
</td>
|
|
3231
|
+
<td>
|
|
3232
|
+
<p-tag
|
|
3233
|
+
[value]="task.priority"
|
|
3234
|
+
[severity]="prioritySeverity(task.priority)"
|
|
3235
|
+
/>
|
|
3236
|
+
</td>
|
|
3237
|
+
<td>
|
|
3238
|
+
@if (task.assignees?.length) {
|
|
3239
|
+
<div class="flex items-center -space-x-1.5">
|
|
3240
|
+
@for (a of task.assignees!.slice(0, 3); track a.userId) {
|
|
3241
|
+
<span
|
|
3242
|
+
class="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white flex-shrink-0 ring-2 ring-white dark:ring-surface-800"
|
|
3243
|
+
[style.background-color]="memberColorById(a.userId)"
|
|
3244
|
+
[pTooltip]="a.userName ?? ''"
|
|
3245
|
+
>
|
|
3246
|
+
{{ getMemberInitials(a.userId) }}
|
|
3247
|
+
</span>
|
|
3248
|
+
}
|
|
3249
|
+
@if (task.assignees!.length > 3) {
|
|
3250
|
+
<span
|
|
3251
|
+
class="w-6 h-6 rounded-full bg-surface-200 dark:bg-surface-700 flex items-center justify-center text-[10px] font-bold text-surface-600 ring-2 ring-white dark:ring-surface-800 flex-shrink-0"
|
|
3252
|
+
>
|
|
3253
|
+
+{{ task.assignees!.length - 3 }}
|
|
3254
|
+
</span>
|
|
3255
|
+
}
|
|
3256
|
+
</div>
|
|
3257
|
+
} @else {
|
|
3258
|
+
<span class="text-muted-color text-sm">—</span>
|
|
3259
|
+
}
|
|
3260
|
+
</td>
|
|
3261
|
+
<td>
|
|
3262
|
+
@if (task.dueDate) {
|
|
3263
|
+
<span
|
|
3264
|
+
class="text-sm"
|
|
3265
|
+
[class.text-red-500]="isOverdue(task.dueDate)"
|
|
3266
|
+
>
|
|
3267
|
+
{{ task.dueDate | date: 'MMM d, y' }}
|
|
3268
|
+
</span>
|
|
3269
|
+
} @else {
|
|
3270
|
+
<span class="text-muted-color text-sm">—</span>
|
|
3271
|
+
}
|
|
3272
|
+
</td>
|
|
3273
|
+
<td>
|
|
3274
|
+
<div class="flex gap-1">
|
|
3275
|
+
<p-button
|
|
3276
|
+
icon="pi pi-pencil"
|
|
3277
|
+
[text]="true"
|
|
3278
|
+
severity="secondary"
|
|
3279
|
+
size="small"
|
|
3280
|
+
[pTooltip]="'shared.edit' | translate"
|
|
3281
|
+
(onClick)="onEditTask(task)"
|
|
3282
|
+
/>
|
|
3283
|
+
<p-button
|
|
3284
|
+
icon="pi pi-trash"
|
|
3285
|
+
[text]="true"
|
|
3286
|
+
severity="danger"
|
|
3287
|
+
size="small"
|
|
3288
|
+
[pTooltip]="'shared.delete' | translate"
|
|
3289
|
+
(onClick)="onDeleteTask(task)"
|
|
3290
|
+
/>
|
|
3291
|
+
</div>
|
|
3292
|
+
</td>
|
|
3293
|
+
</tr>
|
|
3294
|
+
</ng-template>
|
|
3295
|
+
<ng-template #emptymessage>
|
|
3296
|
+
<tr>
|
|
3297
|
+
<td colspan="6" class="text-center text-muted-color py-8">
|
|
3298
|
+
<i class="pi pi-inbox text-4xl block mb-3"></i>
|
|
3299
|
+
{{ 'task.manager.table.empty' | translate }}
|
|
3300
|
+
</td>
|
|
3301
|
+
</tr>
|
|
3302
|
+
</ng-template>
|
|
3303
|
+
</p-table>
|
|
3304
|
+
</div>
|
|
3305
|
+
</div>
|
|
3306
|
+
</div>
|
|
3307
|
+
}
|
|
3308
|
+
</div>
|
|
3309
|
+
|
|
3310
|
+
<tm-task-form-dialog
|
|
3311
|
+
[(visible)]="showTaskForm"
|
|
3312
|
+
[task]="editingTask()"
|
|
3313
|
+
[boardId]="boardId()"
|
|
3314
|
+
[listId]="activeListId()"
|
|
3315
|
+
[members]="state.members()"
|
|
3316
|
+
[labels]="state.labels()"
|
|
3317
|
+
(saved)="onTaskSaved($event)"
|
|
3318
|
+
/>
|
|
3319
|
+
|
|
3320
|
+
<tm-task-detail-panel
|
|
3321
|
+
[task]="state.selectedTask()"
|
|
3322
|
+
(closePanel)="state.selectTask(null)"
|
|
3323
|
+
(editTask)="onEditTask($event)"
|
|
3324
|
+
/>
|
|
3325
|
+
|
|
3326
|
+
<p-dialog
|
|
3327
|
+
[header]="'task.manager.label.manage.title' | translate"
|
|
3328
|
+
[(visible)]="showLabelsDialog"
|
|
3329
|
+
[modal]="true"
|
|
3330
|
+
[style]="{ width: '420px' }"
|
|
3331
|
+
(onHide)="newLabelName = ''; newLabelColor = '#6366F1'"
|
|
3332
|
+
>
|
|
3333
|
+
<div class="flex flex-col gap-4 py-2">
|
|
3334
|
+
<div class="flex gap-2 items-end">
|
|
3335
|
+
<div class="flex flex-col gap-1 flex-1">
|
|
3336
|
+
<label class="text-sm font-medium">{{
|
|
3337
|
+
'task.manager.label.name.label' | translate
|
|
3338
|
+
}}</label>
|
|
3339
|
+
<input
|
|
3340
|
+
pInputText
|
|
3341
|
+
[(ngModel)]="newLabelName"
|
|
3342
|
+
[placeholder]="'task.manager.label.name.placeholder' | translate"
|
|
3343
|
+
/>
|
|
3344
|
+
</div>
|
|
3345
|
+
<div class="flex flex-col gap-1">
|
|
3346
|
+
<label class="text-sm font-medium">{{
|
|
3347
|
+
'task.manager.label.color.label' | translate
|
|
3348
|
+
}}</label>
|
|
3349
|
+
<input
|
|
3350
|
+
type="color"
|
|
3351
|
+
[(ngModel)]="newLabelColor"
|
|
3352
|
+
class="w-10 h-10 rounded cursor-pointer border border-surface-300"
|
|
3353
|
+
/>
|
|
3354
|
+
</div>
|
|
3355
|
+
<p-button
|
|
3356
|
+
icon="pi pi-plus"
|
|
3357
|
+
[label]="'task.manager.label.add' | translate"
|
|
3358
|
+
size="small"
|
|
3359
|
+
[loading]="isAddingLabel()"
|
|
3360
|
+
(onClick)="addLabel()"
|
|
3361
|
+
/>
|
|
3362
|
+
</div>
|
|
3363
|
+
|
|
3364
|
+
<div class="flex flex-col gap-2 max-h-64 overflow-y-auto">
|
|
3365
|
+
@if (state.labels().length === 0) {
|
|
3366
|
+
<p class="text-sm text-surface-400 text-center py-4">
|
|
3367
|
+
{{ 'task.manager.label.no.labels' | translate }}
|
|
3368
|
+
</p>
|
|
3369
|
+
}
|
|
3370
|
+
@for (label of state.labels(); track label.id) {
|
|
3371
|
+
<div
|
|
3372
|
+
class="flex items-center justify-between px-3 py-2 rounded-lg border border-surface-200 dark:border-surface-700"
|
|
3373
|
+
>
|
|
3374
|
+
<div class="flex items-center gap-2">
|
|
3375
|
+
<span
|
|
3376
|
+
class="w-4 h-4 rounded-full flex-shrink-0"
|
|
3377
|
+
[style.background-color]="label.color"
|
|
3378
|
+
></span>
|
|
3379
|
+
<span class="text-sm font-medium">{{ label.name }}</span>
|
|
3380
|
+
</div>
|
|
3381
|
+
<p-button
|
|
3382
|
+
icon="pi pi-trash"
|
|
3383
|
+
severity="danger"
|
|
3384
|
+
size="small"
|
|
3385
|
+
[text]="true"
|
|
3386
|
+
(onClick)="removeLabel(label.id)"
|
|
3387
|
+
/>
|
|
3388
|
+
</div>
|
|
3389
|
+
}
|
|
3390
|
+
</div>
|
|
3391
|
+
</div>
|
|
3392
|
+
</p-dialog>
|
|
3393
|
+
|
|
3394
|
+
<p-dialog
|
|
3395
|
+
[header]="'task.manager.member.manage.title' | translate"
|
|
3396
|
+
[(visible)]="showMembersDialog"
|
|
3397
|
+
[modal]="true"
|
|
3398
|
+
[style]="{ width: '480px' }"
|
|
3399
|
+
(onHide)="resetMemberForm()"
|
|
3400
|
+
>
|
|
3401
|
+
<div class="flex flex-col gap-4 py-2">
|
|
3402
|
+
<div class="flex flex-col gap-3">
|
|
3403
|
+
<div class="flex flex-col gap-1">
|
|
3404
|
+
<label class="text-sm font-medium">{{
|
|
3405
|
+
'task.manager.member.user.label' | translate
|
|
3406
|
+
}}</label>
|
|
3407
|
+
<lib-user-select
|
|
3408
|
+
[(value)]="selectedMemberUser"
|
|
3409
|
+
[isEditMode]="true"
|
|
3410
|
+
[placeHolder]="'task.manager.member.user.placeholder' | translate"
|
|
3411
|
+
/>
|
|
3412
|
+
</div>
|
|
3413
|
+
|
|
3414
|
+
<div class="flex gap-2 items-end">
|
|
3415
|
+
<div class="flex flex-col gap-1 flex-1">
|
|
3416
|
+
<label class="text-sm font-medium">{{
|
|
3417
|
+
'task.manager.member.role.label' | translate
|
|
3418
|
+
}}</label>
|
|
3419
|
+
<p-select
|
|
3420
|
+
[(ngModel)]="newMemberRole"
|
|
3421
|
+
[options]="memberRoleOptions"
|
|
3422
|
+
optionValue="value"
|
|
3423
|
+
[placeholder]="
|
|
3424
|
+
'task.manager.member.role.placeholder' | translate
|
|
3425
|
+
"
|
|
3426
|
+
class="w-full"
|
|
3427
|
+
appendTo="body"
|
|
3428
|
+
>
|
|
3429
|
+
<ng-template #selectedItem let-opt>
|
|
3430
|
+
{{ opt.labelKey | translate }}
|
|
3431
|
+
</ng-template>
|
|
3432
|
+
<ng-template #item let-opt>
|
|
3433
|
+
{{ opt.labelKey | translate }}
|
|
3434
|
+
</ng-template>
|
|
3435
|
+
</p-select>
|
|
3436
|
+
</div>
|
|
3437
|
+
<p-button
|
|
3438
|
+
icon="pi pi-plus"
|
|
3439
|
+
[label]="'task.manager.member.add' | translate"
|
|
3440
|
+
size="small"
|
|
3441
|
+
[loading]="isAddingMember()"
|
|
3442
|
+
[disabled]="!selectedMemberUser"
|
|
3443
|
+
(onClick)="addMember()"
|
|
3444
|
+
/>
|
|
3445
|
+
</div>
|
|
3446
|
+
</div>
|
|
3447
|
+
|
|
3448
|
+
<div class="flex flex-col gap-2 max-h-72 overflow-y-auto">
|
|
3449
|
+
@if (state.members().length === 0) {
|
|
3450
|
+
<p class="text-sm text-surface-400 text-center py-4">
|
|
3451
|
+
{{ 'task.manager.member.no.members' | translate }}
|
|
3452
|
+
</p>
|
|
3453
|
+
}
|
|
3454
|
+
@for (member of state.members(); track member.id) {
|
|
3455
|
+
<div
|
|
3456
|
+
class="flex items-center justify-between px-3 py-2.5 rounded-lg border border-surface-200 dark:border-surface-700"
|
|
3457
|
+
>
|
|
3458
|
+
<div class="flex items-center gap-3">
|
|
3459
|
+
<div
|
|
3460
|
+
class="w-8 h-8 rounded-full flex items-center justify-center text-[11px] font-bold text-white flex-shrink-0"
|
|
3461
|
+
[style.background-color]="memberColor(member)"
|
|
3462
|
+
>
|
|
3463
|
+
{{ memberInitials(member) }}
|
|
3464
|
+
</div>
|
|
3465
|
+
<div>
|
|
3466
|
+
<div class="text-sm font-medium ">
|
|
3467
|
+
{{ member.userName ?? member.userId }}
|
|
3468
|
+
</div>
|
|
3469
|
+
@if (member.userEmail) {
|
|
3470
|
+
<div class="text-xs text-surface-400">
|
|
3471
|
+
{{ member.userEmail }}
|
|
3472
|
+
</div>
|
|
3473
|
+
}
|
|
3474
|
+
</div>
|
|
3475
|
+
</div>
|
|
3476
|
+
<div class="flex items-center gap-2">
|
|
3477
|
+
<span
|
|
3478
|
+
class="text-[10px] font-semibold px-2 py-0.5 rounded-full"
|
|
3479
|
+
[class]="roleBadgeClass(member.role)"
|
|
3480
|
+
>
|
|
3481
|
+
{{ ('task.manager.member.role.' + member.role.toLowerCase()) | translate }}
|
|
3482
|
+
</span>
|
|
3483
|
+
@if (member.role !== 'OWNER') {
|
|
3484
|
+
<p-button
|
|
3485
|
+
icon="pi pi-trash"
|
|
3486
|
+
severity="danger"
|
|
3487
|
+
size="small"
|
|
3488
|
+
[text]="true"
|
|
3489
|
+
(onClick)="removeMember(member.userId)"
|
|
3490
|
+
/>
|
|
3491
|
+
}
|
|
3492
|
+
</div>
|
|
3493
|
+
</div>
|
|
3494
|
+
}
|
|
3495
|
+
</div>
|
|
3496
|
+
</div>
|
|
3497
|
+
</p-dialog>
|
|
3498
|
+
|
|
3499
|
+
<p-dialog
|
|
3500
|
+
[header]="'task.manager.list.new' | translate"
|
|
3501
|
+
[(visible)]="showAddListDialog"
|
|
3502
|
+
[modal]="true"
|
|
3503
|
+
[style]="{ width: '360px' }"
|
|
3504
|
+
(onHide)="newListName = ''"
|
|
3505
|
+
>
|
|
3506
|
+
<div class="flex flex-col gap-2 py-2">
|
|
3507
|
+
<label class="text-sm font-medium">
|
|
3508
|
+
{{ 'task.manager.list.name.label' | translate }}
|
|
3509
|
+
<span class="text-red-500">*</span>
|
|
3510
|
+
</label>
|
|
3511
|
+
<input
|
|
3512
|
+
pInputText
|
|
3513
|
+
[(ngModel)]="newListName"
|
|
3514
|
+
[placeholder]="'task.manager.list.name.placeholder' | translate"
|
|
3515
|
+
/>
|
|
3516
|
+
</div>
|
|
3517
|
+
<ng-template pTemplate="footer">
|
|
3518
|
+
<div class="flex justify-end gap-2">
|
|
3519
|
+
<p-button
|
|
3520
|
+
[label]="'shared.cancel' | translate"
|
|
3521
|
+
severity="secondary"
|
|
3522
|
+
(onClick)="showAddListDialog.set(false)"
|
|
3523
|
+
/>
|
|
3524
|
+
<p-button
|
|
3525
|
+
[label]="'shared.create' | translate"
|
|
3526
|
+
[loading]="isAddingList()"
|
|
3527
|
+
(onClick)="addList()"
|
|
3528
|
+
/>
|
|
3529
|
+
</div>
|
|
3530
|
+
</ng-template>
|
|
3531
|
+
</p-dialog>
|
|
3532
|
+
|
|
3533
|
+
<p-dialog
|
|
3534
|
+
[header]="'task.manager.board.edit' | translate"
|
|
3535
|
+
[(visible)]="showEditBoardDialog"
|
|
3536
|
+
[modal]="true"
|
|
3537
|
+
[style]="{ width: '420px' }"
|
|
3538
|
+
>
|
|
3539
|
+
<div class="flex flex-col gap-4 py-2">
|
|
3540
|
+
<div class="flex flex-col gap-1">
|
|
3541
|
+
<label class="text-sm font-medium">
|
|
3542
|
+
{{ 'task.manager.board.name.label' | translate }}
|
|
3543
|
+
<span class="text-red-500">*</span>
|
|
3544
|
+
</label>
|
|
3545
|
+
<input pInputText [(ngModel)]="editBoardName" />
|
|
3546
|
+
</div>
|
|
3547
|
+
<div class="flex flex-col gap-1">
|
|
3548
|
+
<label class="text-sm font-medium">{{
|
|
3549
|
+
'task.manager.board.description.label' | translate
|
|
3550
|
+
}}</label>
|
|
3551
|
+
<textarea
|
|
3552
|
+
pTextarea
|
|
3553
|
+
[(ngModel)]="editBoardDescription"
|
|
3554
|
+
[rows]="3"
|
|
3555
|
+
[placeholder]="'task.manager.board.description.placeholder' | translate"
|
|
3556
|
+
></textarea>
|
|
3557
|
+
</div>
|
|
3558
|
+
<div class="flex flex-col gap-1">
|
|
3559
|
+
<label class="text-sm font-medium">{{
|
|
3560
|
+
'task.manager.board.color.label' | translate
|
|
3561
|
+
}}</label>
|
|
3562
|
+
<div class="flex items-center gap-3">
|
|
3563
|
+
<input
|
|
3564
|
+
type="color"
|
|
3565
|
+
[(ngModel)]="editBoardColor"
|
|
3566
|
+
class="w-10 h-10 rounded cursor-pointer border border-surface-300"
|
|
3567
|
+
/>
|
|
3568
|
+
<div class="flex gap-2 flex-wrap">
|
|
3569
|
+
@for (color of boardColorPalette; track color) {
|
|
3570
|
+
<button
|
|
3571
|
+
type="button"
|
|
3572
|
+
class="w-6 h-6 rounded-full border-2 transition-transform hover:scale-110"
|
|
3573
|
+
[style.background-color]="color"
|
|
3574
|
+
[class.border-surface-900]="editBoardColor === color"
|
|
3575
|
+
[class.border-transparent]="editBoardColor !== color"
|
|
3576
|
+
(click)="editBoardColor = color"
|
|
3577
|
+
></button>
|
|
3578
|
+
}
|
|
3579
|
+
</div>
|
|
3580
|
+
</div>
|
|
3581
|
+
</div>
|
|
3582
|
+
</div>
|
|
3583
|
+
<ng-template pTemplate="footer">
|
|
3584
|
+
<div class="flex justify-end gap-2">
|
|
3585
|
+
<p-button
|
|
3586
|
+
[label]="'shared.cancel' | translate"
|
|
3587
|
+
severity="secondary"
|
|
3588
|
+
(onClick)="showEditBoardDialog.set(false)"
|
|
3589
|
+
/>
|
|
3590
|
+
<p-button
|
|
3591
|
+
[label]="'shared.save' | translate"
|
|
3592
|
+
[loading]="isSavingBoard()"
|
|
3593
|
+
[disabled]="!editBoardName.trim()"
|
|
3594
|
+
(onClick)="saveBoard()"
|
|
3595
|
+
/>
|
|
3596
|
+
</div>
|
|
3597
|
+
</ng-template>
|
|
3598
|
+
</p-dialog>
|
|
3599
|
+
`,
|
|
3600
|
+
}]
|
|
3601
|
+
}], ctorParameters: () => [], propDecorators: { boardId: [{ type: i0.Input, args: [{ isSignal: true, alias: "boardId", required: true }] }] } });
|
|
3602
|
+
|
|
3603
|
+
var boardViewPage_component = /*#__PURE__*/Object.freeze({
|
|
3604
|
+
__proto__: null,
|
|
3605
|
+
BoardViewPageComponent: BoardViewPageComponent
|
|
3606
|
+
});
|
|
3607
|
+
|
|
3608
|
+
const TASK_MANAGER_MESSAGES = {
|
|
3609
|
+
// Board API messages (keys must match NestJS message-keys.ts)
|
|
3610
|
+
'task.board.not.found': 'Board not found',
|
|
3611
|
+
'task.board.create.success': 'Board created successfully',
|
|
3612
|
+
'task.board.create.many.success': 'Boards created successfully',
|
|
3613
|
+
'task.board.get.success': 'Board retrieved successfully',
|
|
3614
|
+
'task.board.get.all.success': 'Boards retrieved successfully',
|
|
3615
|
+
'task.board.update.success': 'Board updated successfully',
|
|
3616
|
+
'task.board.update.many.success': 'Boards updated successfully',
|
|
3617
|
+
'task.board.delete.success': 'Board deleted successfully',
|
|
3618
|
+
'task.board.restore.success': 'Board restored successfully',
|
|
3619
|
+
'task.board.member.added': 'Member added to board',
|
|
3620
|
+
'task.board.member.removed': 'Member removed from board',
|
|
3621
|
+
'task.board.member.not.found': 'Board member not found',
|
|
3622
|
+
'task.board.member.already.exists': 'User is already a board member',
|
|
3623
|
+
'task.board.member.get.all.success': 'Board members retrieved successfully',
|
|
3624
|
+
'task.list.not.found': 'List not found',
|
|
3625
|
+
'task.list.create.success': 'List created successfully',
|
|
3626
|
+
'task.list.create.many.success': 'Lists created successfully',
|
|
3627
|
+
'task.list.get.success': 'List retrieved successfully',
|
|
3628
|
+
'task.list.get.all.success': 'Lists retrieved successfully',
|
|
3629
|
+
'task.list.update.success': 'List updated successfully',
|
|
3630
|
+
'task.list.update.many.success': 'Lists updated successfully',
|
|
3631
|
+
'task.list.delete.success': 'List deleted successfully',
|
|
3632
|
+
'task.list.restore.success': 'List restored successfully',
|
|
3633
|
+
'task.list.reorder.success': 'Lists reordered successfully',
|
|
3634
|
+
'task.not.found': 'Task not found',
|
|
3635
|
+
'task.create.success': 'Task created successfully',
|
|
3636
|
+
'task.create.many.success': 'Tasks created successfully',
|
|
3637
|
+
'task.get.success': 'Task retrieved successfully',
|
|
3638
|
+
'task.get.all.success': 'Tasks retrieved successfully',
|
|
3639
|
+
'task.update.success': 'Task updated successfully',
|
|
3640
|
+
'task.update.many.success': 'Tasks updated successfully',
|
|
3641
|
+
'task.delete.success': 'Task deleted successfully',
|
|
3642
|
+
'task.restore.success': 'Task restored successfully',
|
|
3643
|
+
'task.move.success': 'Task moved successfully',
|
|
3644
|
+
'task.assign.success': 'Task assigned successfully',
|
|
3645
|
+
'task.assignee.not.board.member': 'Assignee is not a board member',
|
|
3646
|
+
'task.label.not.found': 'Label not found',
|
|
3647
|
+
'task.label.create.success': 'Label created successfully',
|
|
3648
|
+
'task.label.create.many.success': 'Labels created successfully',
|
|
3649
|
+
'task.label.get.success': 'Label retrieved successfully',
|
|
3650
|
+
'task.label.get.all.success': 'Labels retrieved successfully',
|
|
3651
|
+
'task.label.update.success': 'Label updated successfully',
|
|
3652
|
+
'task.label.update.many.success': 'Labels updated successfully',
|
|
3653
|
+
'task.label.delete.success': 'Label deleted successfully',
|
|
3654
|
+
'task.label.restore.success': 'Label restored successfully',
|
|
3655
|
+
'task.comment.not.found': 'Comment not found',
|
|
3656
|
+
'task.comment.create.success': 'Comment added successfully',
|
|
3657
|
+
'task.comment.create.many.success': 'Comments added successfully',
|
|
3658
|
+
'task.comment.get.success': 'Comment retrieved successfully',
|
|
3659
|
+
'task.comment.get.all.success': 'Comments retrieved successfully',
|
|
3660
|
+
'task.comment.update.success': 'Comment updated successfully',
|
|
3661
|
+
'task.comment.update.many.success': 'Comments updated successfully',
|
|
3662
|
+
'task.comment.delete.success': 'Comment deleted successfully',
|
|
3663
|
+
'task.comment.restore.success': 'Comment restored successfully',
|
|
3664
|
+
'task.comment.forbidden': "Cannot edit another user's comment",
|
|
3665
|
+
'task.manager.boards.title': 'Task Boards',
|
|
3666
|
+
'task.manager.boards.subtitle': 'Manage and organize your project boards',
|
|
3667
|
+
'task.manager.board.new': 'New Board',
|
|
3668
|
+
'task.manager.board.name.placeholder': 'e.g. Product Backlog',
|
|
3669
|
+
'task.manager.board.description.placeholder': 'Optional description',
|
|
3670
|
+
'task.manager.board.color.label': 'Board Color',
|
|
3671
|
+
'task.manager.board.empty': 'No boards yet',
|
|
3672
|
+
'task.manager.board.empty.hint': 'Create your first board to get started',
|
|
3673
|
+
'task.manager.board.members': 'Members',
|
|
3674
|
+
'task.manager.board.lists': 'Lists',
|
|
3675
|
+
'task.manager.board.tasks': 'Tasks',
|
|
3676
|
+
'task.manager.board.add.list': 'Add List',
|
|
3677
|
+
'task.manager.board.add.task': 'Add task',
|
|
3678
|
+
'task.manager.board.drop.tasks.here': 'Drop tasks here',
|
|
3679
|
+
'task.manager.board.list.empty': 'No lists yet. Click "Add List" to create one.',
|
|
3680
|
+
'task.manager.list.new': 'New List',
|
|
3681
|
+
'task.manager.list.name.label': 'List Name',
|
|
3682
|
+
'task.manager.list.name.placeholder': 'e.g. In Progress',
|
|
3683
|
+
'task.manager.list.rename': 'Rename List',
|
|
3684
|
+
'task.manager.board.manage.members': 'Members',
|
|
3685
|
+
'task.manager.member.manage.title': 'Manage Members',
|
|
3686
|
+
'task.manager.member.add': 'Add Member',
|
|
3687
|
+
'task.manager.member.no.members': 'No members yet.',
|
|
3688
|
+
'task.manager.member.user.label': 'User',
|
|
3689
|
+
'task.manager.member.user.placeholder': 'Search by name or email',
|
|
3690
|
+
'task.manager.member.role.label': 'Role',
|
|
3691
|
+
'task.manager.member.role.placeholder': 'Select role',
|
|
3692
|
+
'task.manager.member.role.owner': 'Owner',
|
|
3693
|
+
'task.manager.member.role.admin': 'Admin',
|
|
3694
|
+
'task.manager.member.role.member': 'Member',
|
|
3695
|
+
'task.manager.member.role.viewer': 'Viewer',
|
|
3696
|
+
'task.manager.board.manage.labels': 'Labels',
|
|
3697
|
+
'task.manager.label.manage.title': 'Manage Labels',
|
|
3698
|
+
'task.manager.label.name.label': 'Label Name',
|
|
3699
|
+
'task.manager.label.name.placeholder': 'e.g. Bug, Feature',
|
|
3700
|
+
'task.manager.label.color.label': 'Color',
|
|
3701
|
+
'task.manager.label.add': 'Add Label',
|
|
3702
|
+
'task.manager.label.no.labels': 'No labels yet.',
|
|
3703
|
+
'task.manager.board.edit': 'Edit Board',
|
|
3704
|
+
'task.manager.board.name.label': 'Board Name',
|
|
3705
|
+
'task.manager.board.description.label': 'Description',
|
|
3706
|
+
'task.manager.view.board': 'Board',
|
|
3707
|
+
'task.manager.view.list': 'List',
|
|
3708
|
+
'task.manager.table.title': 'Title',
|
|
3709
|
+
'task.manager.table.list': 'List',
|
|
3710
|
+
'task.manager.table.priority': 'Priority',
|
|
3711
|
+
'task.manager.table.assignee': 'Assignee',
|
|
3712
|
+
'task.manager.table.due.date': 'Due Date',
|
|
3713
|
+
'task.manager.table.empty': 'No tasks yet.',
|
|
3714
|
+
'task.form.title.create': 'New Task',
|
|
3715
|
+
'task.form.title.edit': 'Edit Task',
|
|
3716
|
+
'task.form.title.label': 'Title',
|
|
3717
|
+
'task.form.title.placeholder': 'Task title',
|
|
3718
|
+
'task.form.description.placeholder': 'Add a description...',
|
|
3719
|
+
'task.form.priority.label': 'Priority',
|
|
3720
|
+
'task.form.priority.placeholder': 'Select priority',
|
|
3721
|
+
'task.form.due.date.label': 'Due Date',
|
|
3722
|
+
'task.form.due.date.placeholder': 'Pick a date',
|
|
3723
|
+
'task.form.assignee.label': 'Assignee',
|
|
3724
|
+
'task.form.assignee.placeholder': 'Assign to board member',
|
|
3725
|
+
'task.form.labels.label': 'Labels',
|
|
3726
|
+
'task.form.labels.placeholder': 'Select labels',
|
|
3727
|
+
'task.priority.LOW': 'Low',
|
|
3728
|
+
'task.priority.MEDIUM': 'Medium',
|
|
3729
|
+
'task.priority.HIGH': 'High',
|
|
3730
|
+
'task.priority.URGENT': 'Urgent',
|
|
3731
|
+
'task.detail.description.empty': 'No description',
|
|
3732
|
+
'task.detail.comments.label': 'Comments',
|
|
3733
|
+
'task.detail.comments.empty': 'No comments yet',
|
|
3734
|
+
'task.detail.comment.placeholder': 'Add a comment...',
|
|
3735
|
+
'task.detail.due.label': 'Due',
|
|
3736
|
+
'task.detail.assignees.label': 'Assignees',
|
|
3737
|
+
'task.detail.edit': 'Edit task',
|
|
3738
|
+
'task.detail.delete': 'Delete task',
|
|
3739
|
+
'task.comment.reply': 'Reply',
|
|
3740
|
+
'task.comment.reply.placeholder': 'Write a reply...',
|
|
3741
|
+
};
|
|
3742
|
+
|
|
3743
|
+
const TASK_MANAGER_ROUTES = [
|
|
3744
|
+
{
|
|
3745
|
+
path: '',
|
|
3746
|
+
resolve: {
|
|
3747
|
+
translations: resolveTranslationModule({
|
|
3748
|
+
modules: ['task-manager'],
|
|
3749
|
+
fallbackMessages: { ...TASK_MANAGER_MESSAGES, ...SHARED_MESSAGES },
|
|
3750
|
+
}),
|
|
3751
|
+
},
|
|
3752
|
+
loadComponent: () => Promise.resolve().then(function () { return boardListPage_component; }).then((m) => m.BoardListPageComponent),
|
|
3753
|
+
},
|
|
3754
|
+
{
|
|
3755
|
+
path: 'board/:boardId',
|
|
3756
|
+
resolve: {
|
|
3757
|
+
translations: resolveTranslationModule({
|
|
3758
|
+
modules: ['task-manager'],
|
|
3759
|
+
fallbackMessages: { ...TASK_MANAGER_MESSAGES, ...SHARED_MESSAGES },
|
|
3760
|
+
}),
|
|
3761
|
+
},
|
|
3762
|
+
loadComponent: () => Promise.resolve().then(function () { return boardViewPage_component; }).then((m) => m.BoardViewPageComponent),
|
|
3763
|
+
},
|
|
3764
|
+
];
|
|
3765
|
+
|
|
3766
|
+
/**
|
|
3767
|
+
* Generated bundle index. Do not edit.
|
|
3768
|
+
*/
|
|
3769
|
+
|
|
3770
|
+
export { BOARD_COLOR_PALETTE, BoardListPageComponent, BoardViewPageComponent, MEMBER_COLOR_PALETTE, TASK_MANAGER_MESSAGES, TASK_MANAGER_ROUTES, TaskApiService, TaskBoardApiService, TaskBoardSocketService, TaskBoardStateService, TaskCardComponent, TaskCommentApiService, TaskDetailPanelComponent, TaskFormDialogComponent, TaskLabelApiService, TaskListApiService, isTaskOverdue, memberColorFromId, memberInitials, taskPrioritySeverity };
|
|
3771
|
+
//# sourceMappingURL=flusys-ng-task-manager.mjs.map
|