@flusys/ng-task-manager 9.1.2 → 9.2.1

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from "@angular/core";
2
- import { Component, DestroyRef, Service, computed, effect, inject, input, model, output, signal } from "@angular/core";
2
+ import { Component, DestroyRef, Service, computed, effect, inject, input, model, output, signal, untracked } from "@angular/core";
3
3
  import { ApiResourceService, SHARED_MESSAGES, TranslatePipe, resolveTranslationModule } from "@flusys/ng-shared";
4
4
  import { Subject, firstValueFrom } from "rxjs";
5
5
  import { io } from "socket.io-client";
@@ -177,35 +177,51 @@ var TaskBoardSocketService = class TaskBoardSocketService {
177
177
  isConnected = this._isConnected.asReadonly();
178
178
  boardEvent$ = new Subject();
179
179
  onBoardEvent = this.boardEvent$.asObservable();
180
+ joinedBoardIds = /* @__PURE__ */ new Set();
180
181
  connect(accessToken) {
181
182
  if (this.socket?.connected) return;
183
+ this.teardown();
182
184
  const baseUrl = this.getBaseUrl();
183
- this.socket = io(`${baseUrl}/task-boards`, {
185
+ const socket = io(`${baseUrl}/task-boards`, {
184
186
  auth: { token: accessToken },
185
187
  transports: ["websocket", "polling"],
186
188
  reconnection: true,
187
189
  reconnectionAttempts: 5,
188
190
  reconnectionDelay: 1e3
189
191
  });
190
- this.socket.on("connect", () => this._isConnected.set(true));
191
- this.socket.on("disconnect", (reason) => {
192
+ this.socket = socket;
193
+ socket.on("connect", () => {
194
+ this._isConnected.set(true);
195
+ this.joinedBoardIds.forEach((boardId) => socket.emit("board:join", { boardId }));
196
+ });
197
+ socket.on("disconnect", (reason) => {
192
198
  this._isConnected.set(false);
193
- if (reason === "io server disconnect") this.socket?.connect();
199
+ if (reason === "io server disconnect") socket.connect();
200
+ });
201
+ socket.on("connect_error", () => this._isConnected.set(false));
202
+ socket.on("board:event", (event) => {
203
+ if (event && typeof event.type === "string" && typeof event.boardId === "string") this.boardEvent$.next(event);
194
204
  });
195
- this.socket.on("connect_error", () => this._isConnected.set(false));
196
- this.socket.on("board:event", (event) => this.boardEvent$.next(event));
197
205
  }
198
206
  joinBoard(boardId) {
199
- this.socket?.emit("board:join", { boardId });
207
+ this.joinedBoardIds.add(boardId);
208
+ if (this.socket?.connected) this.socket.emit("board:join", { boardId });
200
209
  }
201
210
  leaveBoard(boardId) {
202
- this.socket?.emit("board:leave", { boardId });
211
+ this.joinedBoardIds.delete(boardId);
212
+ if (this.socket?.connected) this.socket.emit("board:leave", { boardId });
203
213
  }
204
214
  disconnect() {
205
- this.socket?.disconnect();
206
- this.socket = null;
215
+ this.teardown();
216
+ this.joinedBoardIds.clear();
207
217
  this._isConnected.set(false);
208
218
  }
219
+ teardown() {
220
+ if (!this.socket) return;
221
+ this.socket.removeAllListeners();
222
+ this.socket.disconnect();
223
+ this.socket = null;
224
+ }
209
225
  getBaseUrl() {
210
226
  return this.appConfig.services?.["taskManager"]?.socketUrl ?? this.appConfig.apiBaseUrl;
211
227
  }
@@ -247,6 +263,7 @@ var TaskBoardStateService = class TaskBoardStateService {
247
263
  _selectedTask = signal(null, ...ngDevMode ? [{ debugName: "_selectedTask" }] : /* istanbul ignore next */ []);
248
264
  _isLoading = signal(false, ...ngDevMode ? [{ debugName: "_isLoading" }] : /* istanbul ignore next */ []);
249
265
  _isBoardLoading = signal(false, ...ngDevMode ? [{ debugName: "_isBoardLoading" }] : /* istanbul ignore next */ []);
266
+ openRequestId = 0;
250
267
  boards = this._boards.asReadonly();
251
268
  activeBoard = this._activeBoard.asReadonly();
252
269
  lists = this._lists.asReadonly();
@@ -284,10 +301,11 @@ var TaskBoardStateService = class TaskBoardStateService {
284
301
  }
285
302
  }
286
303
  async openBoard(boardId) {
304
+ const request = ++this.openRequestId;
287
305
  this._isBoardLoading.set(true);
288
306
  try {
289
307
  const prevBoard = this._activeBoard();
290
- if (prevBoard) this.socketService.leaveBoard(prevBoard.id);
308
+ if (prevBoard && prevBoard.id !== boardId) this.socketService.leaveBoard(prevBoard.id);
291
309
  const [boardRes, listsRes, tasksRes, labelsRes, membersRes] = await Promise.all([
292
310
  this.boardApi.findById(boardId),
293
311
  this.listApi.getAll({
@@ -304,19 +322,28 @@ var TaskBoardStateService = class TaskBoardStateService {
304
322
  this.labelApi.getAll({ filter: { boardId } }),
305
323
  this.boardApi.getMembers(boardId)
306
324
  ]);
325
+ if (request !== this.openRequestId) return;
307
326
  this._activeBoard.set(boardRes.data ?? null);
308
327
  this._lists.set(listsRes.data ?? []);
309
328
  this._tasks.set(tasksRes.data ?? []);
310
329
  this._labels.set(labelsRes.data ?? []);
311
330
  this._members.set(membersRes.data ?? []);
312
331
  this.socketService.joinBoard(boardId);
332
+ } catch (error) {
333
+ if (request === this.openRequestId) this.resetBoardState();
334
+ throw error;
313
335
  } finally {
314
- this._isBoardLoading.set(false);
336
+ if (request === this.openRequestId) this._isBoardLoading.set(false);
315
337
  }
316
338
  }
317
339
  closeBoard() {
340
+ this.openRequestId++;
318
341
  const board = this._activeBoard();
319
342
  if (board) this.socketService.leaveBoard(board.id);
343
+ this.resetBoardState();
344
+ this._isBoardLoading.set(false);
345
+ }
346
+ resetBoardState() {
320
347
  this._activeBoard.set(null);
321
348
  this._lists.set([]);
322
349
  this._tasks.set([]);
@@ -356,15 +383,49 @@ var TaskBoardStateService = class TaskBoardStateService {
356
383
  this._members.update((list) => list.filter((m) => m.userId !== userId));
357
384
  }
358
385
  async reorderLists(boardId, orderedIds) {
359
- const posMap = new Map(orderedIds.map((id, i) => [id, i * 100]));
360
- this._lists.update((list) => list.map((l) => posMap.has(l.id) ? {
361
- ...l,
362
- position: posMap.get(l.id)
363
- } : l).sort((a, b) => a.position - b.position));
364
- await this.listApi.reorderLists({
365
- boardId,
366
- orderedIds
367
- });
386
+ const previousPositions = new Map(this._lists().map((l) => [l.id, l.position]));
387
+ this.applyListOrder(orderedIds);
388
+ try {
389
+ await this.listApi.reorderLists({
390
+ boardId,
391
+ orderedIds
392
+ });
393
+ } catch (error) {
394
+ this._lists.update((list) => list.map((l) => previousPositions.has(l.id) ? {
395
+ ...l,
396
+ position: previousPositions.get(l.id)
397
+ } : l).sort((a, b) => a.position - b.position));
398
+ throw error;
399
+ }
400
+ }
401
+ async moveTask(taskId, targetListId, targetIndex) {
402
+ const task = this._tasks().find((t) => t.id === taskId);
403
+ if (!task) return;
404
+ const siblings = this._tasks().filter((t) => t.listId === targetListId && t.id !== taskId).sort((a, b) => a.position - b.position);
405
+ const index = Math.min(Math.max(Math.trunc(targetIndex) || 0, 0), siblings.length);
406
+ const position = positionBetween(siblings[index - 1]?.position, siblings[index]?.position);
407
+ const original = {
408
+ listId: task.listId,
409
+ position: task.position
410
+ };
411
+ this._tasks.update((list) => list.map((t) => t.id === taskId ? {
412
+ ...t,
413
+ listId: targetListId,
414
+ position
415
+ } : t));
416
+ try {
417
+ await this.taskApi.moveTask({
418
+ taskId,
419
+ targetListId,
420
+ position
421
+ });
422
+ } catch (error) {
423
+ this._tasks.update((list) => list.map((t) => t.id === taskId ? {
424
+ ...t,
425
+ ...original
426
+ } : t));
427
+ throw error;
428
+ }
368
429
  }
369
430
  async renameList(listId, name) {
370
431
  await this.listApi.updateList({
@@ -406,7 +467,22 @@ var TaskBoardStateService = class TaskBoardStateService {
406
467
  this._tasks.update((list) => list.filter((t) => t.id !== taskId));
407
468
  if (this._selectedTask()?.id === taskId) this._selectedTask.set(null);
408
469
  }
470
+ applyListOrder(orderedIds) {
471
+ const posMap = new Map(orderedIds.map((id, i) => [id, i * 100]));
472
+ this._lists.update((list) => list.map((l) => posMap.has(l.id) ? {
473
+ ...l,
474
+ position: posMap.get(l.id)
475
+ } : l).sort((a, b) => a.position - b.position));
476
+ }
409
477
  handleBoardEvent(event) {
478
+ const activeBoardId = this._activeBoard()?.id;
479
+ if (!activeBoardId || event.boardId !== activeBoardId) return;
480
+ if (event.type === "list:reordered") {
481
+ const orderedIds = event.payload?.orderedIds;
482
+ if (Array.isArray(orderedIds)) this.applyListOrder(orderedIds.filter((id) => typeof id === "string"));
483
+ return;
484
+ }
485
+ if (!hasId(event.payload)) return;
410
486
  switch (event.type) {
411
487
  case "task:created": {
412
488
  const created = event.payload;
@@ -435,9 +511,11 @@ var TaskBoardStateService = class TaskBoardStateService {
435
511
  if (this._selectedTask()?.id === deleted.id) this._selectedTask.set(null);
436
512
  break;
437
513
  }
438
- case "list:created":
439
- this._lists.update((list) => [...list, event.payload]);
514
+ case "list:created": {
515
+ const created = event.payload;
516
+ this._lists.update((list) => list.some((l) => l.id === created.id) ? list : [...list, created]);
440
517
  break;
518
+ }
441
519
  case "list:updated": {
442
520
  const updatedList = event.payload;
443
521
  this._lists.update((list) => list.map((l) => l.id === updatedList.id ? {
@@ -451,15 +529,6 @@ var TaskBoardStateService = class TaskBoardStateService {
451
529
  this._lists.update((list) => list.filter((l) => l.id !== deletedList.id));
452
530
  break;
453
531
  }
454
- case "list:reordered": {
455
- const { orderedIds } = event.payload;
456
- const posMap = new Map(orderedIds.map((id, i) => [id, i * 100]));
457
- this._lists.update((list) => list.map((l) => posMap.has(l.id) ? {
458
- ...l,
459
- position: posMap.get(l.id)
460
- } : l).sort((a, b) => a.position - b.position));
461
- break;
462
- }
463
532
  }
464
533
  }
465
534
  static ɵfac = i0.ɵɵngDeclareFactory({
@@ -485,6 +554,16 @@ i0.ɵɵngDeclareClassMetadata({
485
554
  decorators: [{ type: Service }],
486
555
  ctorParameters: () => []
487
556
  });
557
+ const MAX_POSITION = 2147483647;
558
+ function positionBetween(before, after) {
559
+ if (before === void 0 && after === void 0) return 0;
560
+ if (before === void 0) return Math.max(0, Math.floor(after / 2));
561
+ if (after === void 0) return Math.min(MAX_POSITION, before + 100);
562
+ return Math.floor((before + after) / 2);
563
+ }
564
+ function hasId(payload) {
565
+ return typeof payload === "object" && payload !== null && typeof payload.id === "string";
566
+ }
488
567
  const MEMBER_COLOR_PALETTE = [
489
568
  "#6366f1",
490
569
  "#8b5cf6",
@@ -510,8 +589,20 @@ function memberInitials(name) {
510
589
  if (!name) return "?";
511
590
  return name.split(" ").slice(0, 2).map((n) => n[0]?.toUpperCase() ?? "").join("");
512
591
  }
592
+ const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})/;
593
+ function toDateOnlyString(date) {
594
+ const month = String(date.getMonth() + 1).padStart(2, "0");
595
+ const day = String(date.getDate()).padStart(2, "0");
596
+ return `${date.getFullYear()}-${month}-${day}`;
597
+ }
598
+ function parseDateOnly(value) {
599
+ const match = value ? DATE_ONLY_PATTERN.exec(value) : null;
600
+ if (!match) return null;
601
+ return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), 12, 0, 0);
602
+ }
513
603
  function isTaskOverdue(dueDate) {
514
- return new Date(dueDate) < /* @__PURE__ */ new Date();
604
+ if (!dueDate || !DATE_ONLY_PATTERN.test(dueDate)) return false;
605
+ return dueDate.slice(0, 10) < toDateOnlyString(/* @__PURE__ */ new Date());
515
606
  }
516
607
  function memberColorFromId(userId) {
517
608
  let hash = 0;
@@ -599,7 +690,7 @@ var TaskCardComponent = class TaskCardComponent {
599
690
 
600
691
  <div class="flex items-center justify-between gap-2 mt-auto pt-2">
601
692
  <f-tag
602
- [value]="task().priority"
693
+ [value]="('task.priority.' + task().priority) | translate"
603
694
  [severity]="prioritySeverity(task().priority)"
604
695
  class="text-xs"
605
696
  />
@@ -775,7 +866,7 @@ i0.ɵɵngDeclareClassMetadata({
775
866
 
776
867
  <div class="flex items-center justify-between gap-2 mt-auto pt-2">
777
868
  <f-tag
778
- [value]="task().priority"
869
+ [value]="('task.priority.' + task().priority) | translate"
779
870
  [severity]="prioritySeverity(task().priority)"
780
871
  class="text-xs"
781
872
  />
@@ -888,7 +979,7 @@ var TaskFormDialogComponent = class TaskFormDialogComponent {
888
979
  description: t.description ?? "",
889
980
  priority: t.priority
890
981
  });
891
- this.dueDate.set(t.dueDate ? new Date(t.dueDate) : null);
982
+ this.dueDate.set(parseDateOnly(t.dueDate));
892
983
  this.selectedAssigneeIds.set(t.assignees?.map((a) => a.userId) ?? []);
893
984
  this.selectedLabelIds.set(t.labelIds ?? []);
894
985
  } else this.reset();
@@ -900,7 +991,7 @@ var TaskFormDialogComponent = class TaskFormDialogComponent {
900
991
  this.isSaving.set(true);
901
992
  try {
902
993
  const dueDate = this.dueDate();
903
- const dueDateStr = dueDate ? dueDate.toISOString().slice(0, 10) : void 0;
994
+ const dueDateStr = dueDate ? toDateOnlyString(dueDate) : null;
904
995
  const existing = this.task();
905
996
  const assigneeIds = this.selectedAssigneeIds();
906
997
  const labelIds = this.selectedLabelIds();
@@ -923,7 +1014,7 @@ var TaskFormDialogComponent = class TaskFormDialogComponent {
923
1014
  title,
924
1015
  description: description || void 0,
925
1016
  priority,
926
- dueDate: dueDateStr,
1017
+ dueDate: dueDateStr ?? void 0,
927
1018
  assigneeIds,
928
1019
  labelIds
929
1020
  };
@@ -1629,14 +1720,15 @@ var TaskDetailPanelComponent = class TaskDetailPanelComponent {
1629
1720
  repliesFor(parentId) {
1630
1721
  return this.comments().filter((c) => c.parentCommentId === parentId);
1631
1722
  }
1723
+ taskId = computed(() => this.task()?.id ?? null, ...ngDevMode ? [{ debugName: "taskId" }] : /* istanbul ignore next */ []);
1632
1724
  constructor() {
1633
1725
  effect(() => {
1634
- const t = this.task();
1635
- if (t) this.loadComments(t.id);
1636
- else {
1726
+ const id = this.taskId();
1727
+ untracked(() => {
1637
1728
  this.comments.set([]);
1638
- this.replyingToId.set(null);
1639
- }
1729
+ this.cancelReply();
1730
+ if (id) this.loadComments(id);
1731
+ });
1640
1732
  });
1641
1733
  }
1642
1734
  toggleReply(commentId) {
@@ -1662,8 +1754,13 @@ var TaskDetailPanelComponent = class TaskDetailPanelComponent {
1662
1754
  prioritySeverity = taskPrioritySeverity;
1663
1755
  initials = memberInitials;
1664
1756
  async loadComments(taskId) {
1665
- const res = await this.commentApi.getAll({ filter: { taskId } });
1666
- this.comments.set(res.data ?? []);
1757
+ let comments = [];
1758
+ try {
1759
+ comments = (await this.commentApi.getAll({ filter: { taskId } })).data ?? [];
1760
+ } catch {
1761
+ comments = [];
1762
+ }
1763
+ if (this.task()?.id === taskId) this.comments.set(comments);
1667
1764
  }
1668
1765
  async addComment(taskId) {
1669
1766
  if (!this.newComment().trim()) return;
@@ -1762,7 +1859,7 @@ var TaskDetailPanelComponent = class TaskDetailPanelComponent {
1762
1859
 
1763
1860
  <div class="flex items-center gap-3">
1764
1861
  <f-tag
1765
- [value]="t.priority"
1862
+ [value]="('task.priority.' + t.priority) | translate"
1766
1863
  [severity]="prioritySeverity(t.priority)"
1767
1864
  />
1768
1865
  @if (t.dueDate) {
@@ -2153,7 +2250,7 @@ i0.ɵɵngDeclareClassMetadata({
2153
2250
 
2154
2251
  <div class="flex items-center gap-3">
2155
2252
  <f-tag
2156
- [value]="t.priority"
2253
+ [value]="('task.priority.' + t.priority) | translate"
2157
2254
  [severity]="prioritySeverity(t.priority)"
2158
2255
  />
2159
2256
  @if (t.dueDate) {
@@ -2479,7 +2576,7 @@ const TASK_MANAGER_ROUTES = [{
2479
2576
  ...SHARED_MESSAGES
2480
2577
  }
2481
2578
  }) },
2482
- loadComponent: () => import("./flusys-ng-task-manager-board-list-page.component-Dmay3IyF.mjs").then((m) => m.BoardListPageComponent)
2579
+ loadComponent: () => import("./flusys-ng-task-manager-board-list-page.component-BAkiTEpy.mjs").then((m) => m.BoardListPageComponent)
2483
2580
  }, {
2484
2581
  path: "board/:boardId",
2485
2582
  resolve: { translations: resolveTranslationModule({
@@ -2489,8 +2586,8 @@ const TASK_MANAGER_ROUTES = [{
2489
2586
  ...SHARED_MESSAGES
2490
2587
  }
2491
2588
  }) },
2492
- loadComponent: () => import("./flusys-ng-task-manager-board-view-page.component-CkC8-UKF.mjs").then((m) => m.BoardViewPageComponent)
2589
+ loadComponent: () => import("./flusys-ng-task-manager-board-view-page.component-9ZrU7hON.mjs").then((m) => m.BoardViewPageComponent)
2493
2590
  }];
2494
- export { BOARD_COLOR_PALETTE, MEMBER_COLOR_PALETTE, TASK_MANAGER_MESSAGES, TASK_MANAGER_ROUTES, TaskApiService, TaskBoardApiService, TaskBoardSocketService, TaskBoardStateService, TaskCardComponent, TaskCommentApiService, TaskDetailPanelComponent, TaskFormDialogComponent, TaskLabelApiService, TaskListApiService, isTaskOverdue, memberColorFromId, memberInitials, taskPrioritySeverity };
2591
+ export { BOARD_COLOR_PALETTE, MEMBER_COLOR_PALETTE, TASK_MANAGER_MESSAGES, TASK_MANAGER_ROUTES, TaskApiService, TaskBoardApiService, TaskBoardSocketService, TaskBoardStateService, TaskCardComponent, TaskCommentApiService, TaskDetailPanelComponent, TaskFormDialogComponent, TaskLabelApiService, TaskListApiService, isTaskOverdue, memberColorFromId, memberInitials, parseDateOnly, taskPrioritySeverity, toDateOnlyString };
2495
2592
 
2496
2593
  //# sourceMappingURL=flusys-ng-task-manager.mjs.map