@tuturuuu/ui 0.2.0 → 0.3.2

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.
Files changed (129) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/package.json +79 -67
  3. package/src/components/ui/__tests__/avatar.test.tsx +8 -5
  4. package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
  5. package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
  6. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
  8. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
  9. package/src/components/ui/chart.test.tsx +29 -0
  10. package/src/components/ui/chart.tsx +12 -3
  11. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +396 -2
  12. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +36 -8
  13. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +14 -0
  14. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +5 -0
  15. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +21 -7
  16. package/src/components/ui/chat/chat-agent-details-utils.test.ts +73 -0
  17. package/src/components/ui/chat/chat-agent-details-utils.tsx +100 -26
  18. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +517 -0
  19. package/src/components/ui/chat/chat-workspace.tsx +31 -1
  20. package/src/components/ui/chat/hooks-messages.test.tsx +45 -1
  21. package/src/components/ui/chat/hooks-messages.ts +1 -1
  22. package/src/components/ui/chat/hooks-realtime.ts +13 -16
  23. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
  24. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
  25. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
  26. package/src/components/ui/custom/common-footer.tsx +16 -1
  27. package/src/components/ui/custom/production-indicator.tsx +1 -1
  28. package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
  29. package/src/components/ui/custom/settings/task-settings.tsx +18 -0
  30. package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
  31. package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
  32. package/src/components/ui/custom/sidebar-context.tsx +61 -61
  33. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
  34. package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
  35. package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
  36. package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
  37. package/src/components/ui/custom/workspace-select.tsx +33 -12
  38. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
  39. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
  40. package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
  41. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
  42. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
  43. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
  44. package/src/components/ui/finance/invoices/hooks.ts +75 -20
  45. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
  46. package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
  47. package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
  48. package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
  49. package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
  50. package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
  51. package/src/components/ui/finance/invoices/utils.test.ts +50 -0
  52. package/src/components/ui/finance/invoices/utils.ts +75 -17
  53. package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
  54. package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
  55. package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
  56. package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
  57. package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
  58. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  59. package/src/components/ui/finance/transactions/form.test.tsx +43 -0
  60. package/src/components/ui/finance/transactions/form.tsx +60 -0
  61. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
  62. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
  63. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
  64. package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
  65. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  66. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
  67. package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
  68. package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
  69. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
  70. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
  71. package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
  72. package/src/components/ui/legacy/meet/page.test.ts +180 -0
  73. package/src/components/ui/legacy/meet/page.tsx +87 -39
  74. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
  75. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
  76. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
  77. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
  78. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
  79. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
  80. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
  81. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
  82. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
  83. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
  84. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
  85. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
  86. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
  87. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
  88. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
  89. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
  90. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
  91. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
  92. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
  93. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
  94. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
  95. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
  96. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
  97. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
  98. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
  99. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
  100. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
  101. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
  102. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
  103. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
  104. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
  105. package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
  106. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
  107. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
  108. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
  109. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
  110. package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
  111. package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
  112. package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
  113. package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
  114. package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
  115. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
  116. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
  117. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
  118. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
  119. package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
  120. package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
  121. package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
  122. package/src/hooks/use-calendar-sync.tsx +22 -277
  123. package/src/hooks/use-calendar.tsx +95 -525
  124. package/src/hooks/use-semantic-task-search.ts +10 -33
  125. package/src/hooks/use-task-actions.ts +43 -117
  126. package/src/hooks/use-user-config.ts +1 -1
  127. package/src/hooks/use-workspace-config.ts +6 -2
  128. package/src/hooks/use-workspace-presence.ts +1 -1
  129. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
@@ -8,7 +8,6 @@ import {
8
8
  Ban,
9
9
  Box,
10
10
  Calendar,
11
- Check,
12
11
  CheckCircle2,
13
12
  CircleSlash,
14
13
  Clock,
@@ -35,6 +34,7 @@ import {
35
34
  listWorkspaceTaskProjects,
36
35
  removeCurrentUserTaskPersonalPlacement,
37
36
  } from '@tuturuuu/internal-api/tasks';
37
+ import type { SupportedColor } from '@tuturuuu/types/primitives/SupportedColors';
38
38
  import type { Task } from '@tuturuuu/types/primitives/Task';
39
39
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
40
40
  import { Badge } from '@tuturuuu/ui/badge';
@@ -49,6 +49,7 @@ import {
49
49
  } from '@tuturuuu/ui/dropdown-menu';
50
50
  import { useCalendarPreferences } from '@tuturuuu/ui/hooks/use-calendar-preferences';
51
51
  import { useTaskActions } from '@tuturuuu/ui/hooks/use-task-actions';
52
+ import { useUserBooleanConfig } from '@tuturuuu/ui/hooks/use-user-config';
52
53
  import { useWorkspaceMembers } from '@tuturuuu/ui/hooks/use-workspace-members';
53
54
  import {
54
55
  HoverCard,
@@ -93,6 +94,11 @@ import { AssigneeSelect } from '../../../shared/assignee-select';
93
94
  import { useBoardBroadcast } from '../../../shared/board-broadcast-context';
94
95
  import { CreateListDialog } from '../../../shared/create-list-dialog';
95
96
  import { formatRelationshipTaskIdentifier } from '../../../shared/relationship-task-identifier';
97
+ import {
98
+ shouldShowTaskDueDate,
99
+ shouldShowTaskStartDate,
100
+ TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID,
101
+ } from '../../../shared/task-due-date-visibility';
96
102
  import { TaskEstimationDisplay } from '../../../shared/task-estimation-display';
97
103
  import { TaskLabelsDisplay } from '../../../shared/task-labels-display';
98
104
  import { TaskShareDialog } from '../../../shared/task-share-dialog';
@@ -121,6 +127,7 @@ import {
121
127
  TaskPriorityMenu,
122
128
  TaskProjectsMenu,
123
129
  TaskRelatedMenu,
130
+ TaskSchedulingMenu,
124
131
  } from '../menus';
125
132
  import { TaskActions } from '../task-actions';
126
133
  import { TaskCustomDateDialog } from '../task-dialogs/TaskCustomDateDialog';
@@ -129,9 +136,16 @@ import { TaskNewLabelDialog } from '../task-dialogs/TaskNewLabelDialog';
129
136
  import { TaskNewProjectDialog } from '../task-dialogs/TaskNewProjectDialog';
130
137
  import { getTaskCardParentBadgeState } from '../task-parent-badge-state';
131
138
  import { TaskCardCheckbox } from './TaskCardCheckbox';
139
+ import {
140
+ getTaskCardSelectionCheckboxToneClasses,
141
+ TASK_CARD_OVERDUE_CHECKBOX_TONE_CLASSES,
142
+ } from './task-card-checkbox-style';
132
143
  import { areTaskCardPropsEqual } from './task-card-comparator';
144
+ import { shouldRenderTaskCardCompletionCheckbox } from './task-card-completion-checkbox-visibility';
145
+ import { TaskCardIdentifierRow } from './task-card-identifier-row';
133
146
  import { mergeTaskCardLabelOptions } from './task-card-label-options';
134
147
  import { getTaskCardVisibilityState } from './task-card-visibility';
148
+ import { TaskSchedulingBadge } from './task-scheduling-badge';
135
149
 
136
150
  export interface TaskCardProps {
137
151
  task: Task;
@@ -186,6 +200,10 @@ function TaskCardInner({
186
200
  const locale = useLocale();
187
201
  const dateLocale = locale === 'vi' ? vi : enUS;
188
202
  const { weekStartsOn, timeFormat } = useCalendarPreferences();
203
+ const { value: showReviewDueDates } = useUserBooleanConfig(
204
+ TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID,
205
+ false
206
+ );
189
207
  const timePattern = getTimeFormatPattern(timeFormat);
190
208
 
191
209
  const [isLoading, setIsLoading] = useState(false);
@@ -673,21 +691,6 @@ function TaskCardInner({
673
691
  menuOpen ||
674
692
  isOptimistic; // Disable drag for optimistic tasks until confirmed
675
693
 
676
- // Debug: log drag state for newly created task
677
- if (task.name === 'new task') {
678
- console.log('[TaskCard Debug]', {
679
- taskId: task.id,
680
- editDialogOpen: dialogState.editDialogOpen,
681
- deleteDialogOpen: dialogState.deleteDialogOpen,
682
- customDateDialogOpen: dialogState.customDateDialogOpen,
683
- newLabelDialogOpen: dialogState.newLabelDialogOpen,
684
- newProjectDialogOpen: dialogState.newProjectDialogOpen,
685
- menuOpen,
686
- isOptimistic,
687
- RESULT_dragDisabled: dragDisabled,
688
- });
689
- }
690
-
691
694
  const sortableDisabled = dragDisabled || isOverlay;
692
695
  const sortableId = isOverlay
693
696
  ? `${task.id}:drag-overlay`
@@ -764,10 +767,32 @@ function TaskCardInner({
764
767
  };
765
768
 
766
769
  const now = new Date();
767
- const isOverdue = task.end_date && new Date(task.end_date) < now;
770
+ const shouldRenderDueDate = shouldShowTaskDueDate({
771
+ completedAt: task.completed_at,
772
+ closedAt: task.closed_at,
773
+ dueDate: task.end_date,
774
+ listStatus: taskList?.status,
775
+ showReviewDueDates,
776
+ });
777
+ const shouldRenderStartDate = shouldShowTaskStartDate({
778
+ completedAt: task.completed_at,
779
+ closedAt: task.closed_at,
780
+ listStatus: taskList?.status,
781
+ startDate: task.start_date,
782
+ });
783
+ const isOverdue = Boolean(
784
+ shouldRenderDueDate && task.end_date && new Date(task.end_date) < now
785
+ );
768
786
  const isResolvedListStatus = isTaskBoardResolvedStatus(taskList?.status);
769
787
  const startDate = task.start_date ? new Date(task.start_date) : null;
770
788
  const endDate = task.end_date ? new Date(task.end_date) : null;
789
+ const selectionCheckboxClassName = cn(
790
+ getTaskCardSelectionCheckboxToneClasses(taskList?.color as SupportedColor),
791
+ isOverdue &&
792
+ !task.closed_at &&
793
+ !isResolvedListStatus &&
794
+ TASK_CARD_OVERDUE_CHECKBOX_TONE_CLASSES
795
+ );
771
796
 
772
797
  // Memoize description metadata to prevent unnecessary recalculations
773
798
  // This is important because descriptionMeta is used in taskBadges dependency array
@@ -1188,6 +1213,36 @@ function TaskCardInner({
1188
1213
  });
1189
1214
  }
1190
1215
 
1216
+ if ((task.total_duration ?? 0) > 0) {
1217
+ badges.push({
1218
+ id: 'duration',
1219
+ element: (
1220
+ <TaskSchedulingBadge
1221
+ key="duration"
1222
+ autoSchedule={task.auto_schedule}
1223
+ calendarHours={task.calendar_hours}
1224
+ isSplittable={task.is_splittable}
1225
+ labels={{
1226
+ autoSchedule: taskBoardT('ws-task-boards.dialog.auto_schedule'),
1227
+ estimatedDuration: taskBoardT(
1228
+ 'ws-task-boards.dialog.estimated_duration'
1229
+ ),
1230
+ meetingHours: taskBoardT('ws-task-boards.dialog.meeting_hours'),
1231
+ personalHours: taskBoardT('ws-task-boards.dialog.personal_hours'),
1232
+ splittable: taskBoardT('ws-task-boards.dialog.splittable'),
1233
+ workHours: taskBoardT('ws-task-boards.dialog.work_hours'),
1234
+ }}
1235
+ maxSplitDurationMinutes={task.max_split_duration_minutes}
1236
+ minSplitDurationMinutes={task.min_split_duration_minutes}
1237
+ onElement={(element) => {
1238
+ if (element) badgeRefs.current.set('duration', element as any);
1239
+ }}
1240
+ totalDuration={task.total_duration}
1241
+ />
1242
+ ),
1243
+ });
1244
+ }
1245
+
1191
1246
  // Labels badge
1192
1247
  if (task.labels && task.labels.length > 0) {
1193
1248
  badges.push({
@@ -1324,6 +1379,12 @@ function TaskCardInner({
1324
1379
  task.priority,
1325
1380
  task.projects,
1326
1381
  task.estimation_points,
1382
+ task.total_duration,
1383
+ task.auto_schedule,
1384
+ task.calendar_hours,
1385
+ task.is_splittable,
1386
+ task.max_split_duration_minutes,
1387
+ task.min_split_duration_minutes,
1327
1388
  task.labels,
1328
1389
  boardConfig?.estimation_type,
1329
1390
  descriptionMeta.totalCheckboxes,
@@ -1336,6 +1397,7 @@ function TaskCardInner({
1336
1397
  blockingCount,
1337
1398
  relatedTaskCount,
1338
1399
  t,
1400
+ taskBoardT,
1339
1401
  parentBadgeIdentifier,
1340
1402
  ]);
1341
1403
 
@@ -1651,65 +1713,35 @@ function TaskCardInner({
1651
1713
  <AlertCircle className="absolute -top-4 -right-4.5 h-3 w-3" />
1652
1714
  </div>
1653
1715
  )}
1654
- {/* Selection indicator */}
1655
- {isMultiSelectMode && (
1656
- <div
1657
- className={cn(
1658
- 'absolute top-2 left-2 flex h-6 w-6 items-center justify-center rounded-full border-2 transition-all duration-200',
1659
- isSelected
1660
- ? 'scale-110 border-primary bg-primary text-primary-foreground shadow-md'
1661
- : 'border-border bg-background/80 text-muted-foreground shadow-sm hover:scale-105 hover:border-primary/50'
1662
- )}
1663
- >
1664
- {isSelected ? (
1665
- <Check className="h-4 w-4 stroke-3" />
1666
- ) : (
1667
- <div className="h-2 w-2 rounded-full bg-current opacity-30" />
1668
- )}
1669
- </div>
1670
- )}
1671
1716
  <div className="p-3">
1672
1717
  {/* Header */}
1673
1718
  <div className="flex items-start gap-1">
1674
1719
  <div className="min-w-0 flex-1">
1675
- <div
1676
- className={cn(
1677
- 'mb-1 flex gap-1',
1678
- isPersonalExternalTask ? 'flex-wrap items-center' : 'flex-col'
1720
+ <TaskCardIdentifierRow
1721
+ externalSourceLabel={externalSourceLabel}
1722
+ externalSourceTitle={[
1723
+ task.source_workspace_name,
1724
+ task.source_board_name,
1725
+ task.source_list_name,
1726
+ ]
1727
+ .filter(Boolean)
1728
+ .join(' / ')}
1729
+ isMultiSelectMode={isMultiSelectMode}
1730
+ isPersonalExternalTask={isPersonalExternalTask}
1731
+ isSelected={isSelected}
1732
+ onSelect={(event) => onSelect?.(task.id, event)}
1733
+ selectTaskLabel={t('select_task', { name: task.name ?? '' })}
1734
+ selectionCheckboxClassName={selectionCheckboxClassName}
1735
+ taskListStatus={taskList?.status}
1736
+ ticketBadgeClassName={getTicketBadgeColorClasses(
1737
+ taskList,
1738
+ task.priority
1679
1739
  )}
1680
- >
1681
- {isPersonalExternalTask && (
1682
- <Badge
1683
- variant="secondary"
1684
- className="h-5 min-w-0 max-w-[70%] gap-1 border border-dynamic-cyan/30 bg-dynamic-cyan/10 px-1.5 text-[10px] text-dynamic-cyan"
1685
- title={[
1686
- task.source_workspace_name,
1687
- task.source_board_name,
1688
- task.source_list_name,
1689
- ]
1690
- .filter(Boolean)
1691
- .join(' / ')}
1692
- >
1693
- <ExternalLink className="h-2.5 w-2.5 shrink-0" />
1694
- <span className="truncate">{externalSourceLabel}</span>
1695
- </Badge>
1696
- )}
1697
- {/* Ticket Identifier */}
1698
- {taskList?.status !== 'documents' && (
1699
- <Badge
1700
- variant="outline"
1701
- className={cn(
1702
- 'w-fit px-1 py-0 font-mono text-[10px]',
1703
- getTicketBadgeColorClasses(taskList, task.priority)
1704
- )}
1705
- title={t('ticket_id_label', {
1706
- id: taskTicketIdentifier,
1707
- })}
1708
- >
1709
- {taskTicketIdentifier}
1710
- </Badge>
1711
- )}
1712
- </div>
1740
+ ticketIdentifier={taskTicketIdentifier}
1741
+ ticketTitle={t('ticket_id_label', {
1742
+ id: taskTicketIdentifier,
1743
+ })}
1744
+ />
1713
1745
  <div className="mb-1">
1714
1746
  {/* Task Name */}
1715
1747
  <button
@@ -1877,6 +1909,47 @@ function TaskCardInner({
1877
1909
  }}
1878
1910
  />
1879
1911
 
1912
+ {/* Scheduling Menu */}
1913
+ {taskList?.status !== 'documents' && (
1914
+ <TaskSchedulingMenu
1915
+ task={task}
1916
+ boardId={boardId}
1917
+ isLoading={isLoading}
1918
+ onUpdate={onUpdate}
1919
+ onClose={() => setMenuOpen(false)}
1920
+ translations={{
1921
+ schedule: taskBoardT('ws-task-boards.dialog.schedule'),
1922
+ estimatedDuration: taskBoardT(
1923
+ 'ws-task-boards.dialog.estimated_duration'
1924
+ ),
1925
+ h: taskBoardT('ws-task-boards.dialog.h'),
1926
+ m: taskBoardT('ws-task-boards.dialog.m'),
1927
+ splittable: taskBoardT(
1928
+ 'ws-task-boards.dialog.splittable'
1929
+ ),
1930
+ minSplit: taskBoardT('ws-task-boards.dialog.min_split'),
1931
+ maxSplit: taskBoardT('ws-task-boards.dialog.max_split'),
1932
+ hourType: taskBoardT('ws-task-boards.dialog.hour_type'),
1933
+ workHours: taskBoardT(
1934
+ 'ws-task-boards.dialog.work_hours'
1935
+ ),
1936
+ meetingHours: taskBoardT(
1937
+ 'ws-task-boards.dialog.meeting_hours'
1938
+ ),
1939
+ personalHours: taskBoardT(
1940
+ 'ws-task-boards.dialog.personal_hours'
1941
+ ),
1942
+ autoSchedule: taskBoardT(
1943
+ 'ws-task-boards.dialog.auto_schedule'
1944
+ ),
1945
+ save: t('save'),
1946
+ clear: t('clear'),
1947
+ saved: t('saved'),
1948
+ error: t('error'),
1949
+ }}
1950
+ />
1951
+ )}
1952
+
1880
1953
  {/* Estimation Menu */}
1881
1954
  {boardConfig?.estimation_type && (
1882
1955
  <TaskEstimationMenu
@@ -2169,11 +2242,10 @@ function TaskCardInner({
2169
2242
  )}
2170
2243
  </div>
2171
2244
  {/* Dates Section (improved layout & conditional rendering) */}
2172
- {/* Hide dates when the list itself represents resolved work. */}
2173
- {(startDate || endDate) && !isResolvedListStatus && (
2245
+ {(shouldRenderStartDate || shouldRenderDueDate) && (
2174
2246
  <div className="mb-1 space-y-0.5 text-[10px] leading-snug">
2175
2247
  {/* Show start only if in the future (hide historical start for visual simplicity) */}
2176
- {startDate && startDate > now && (
2248
+ {shouldRenderStartDate && startDate && startDate > now && (
2177
2249
  <div className="flex items-center gap-1 text-muted-foreground">
2178
2250
  <Clock className="h-2.5 w-2.5 shrink-0" />
2179
2251
  <span className="truncate">
@@ -2191,7 +2263,7 @@ function TaskCardInner({
2191
2263
  </span>
2192
2264
  </div>
2193
2265
  )}
2194
- {endDate && (
2266
+ {shouldRenderDueDate && endDate && (
2195
2267
  <div
2196
2268
  className={cn(
2197
2269
  'flex items-center gap-1',
@@ -2403,7 +2475,10 @@ function TaskCardInner({
2403
2475
  )}
2404
2476
 
2405
2477
  {/* Checkbox: hidden for documents lists */}
2406
- {taskList?.status !== 'documents' && (
2478
+ {shouldRenderTaskCardCompletionCheckbox({
2479
+ isMultiSelectMode,
2480
+ taskListStatus: taskList?.status,
2481
+ }) && (
2407
2482
  <TaskCardCheckbox
2408
2483
  task={task}
2409
2484
  taskList={taskList}
@@ -0,0 +1,174 @@
1
+ import type { CalendarHoursType } from '@tuturuuu/types/primitives/Task';
2
+ import { Badge } from '@tuturuuu/ui/badge';
3
+ import { cn } from '@tuturuuu/utils/format';
4
+ import type { ReactElement, SVGProps } from 'react';
5
+ import {
6
+ formatTaskDurationLabel,
7
+ formatTaskSchedulingBadgeTitle,
8
+ type TaskSchedulingBadgeTitleLabels,
9
+ } from '../menus/task-scheduling-utils';
10
+
11
+ function WorkScheduleIcon(props: SVGProps<SVGSVGElement>) {
12
+ return (
13
+ <svg aria-hidden="true" fill="none" viewBox="0 0 16 16" {...props}>
14
+ <path
15
+ d="M5.75 4.25V3.5A1.5 1.5 0 0 1 7.25 2h1.5a1.5 1.5 0 0 1 1.5 1.5v.75"
16
+ stroke="currentColor"
17
+ strokeLinecap="round"
18
+ strokeWidth="1.5"
19
+ />
20
+ <path
21
+ d="M3.5 4.5h9A1.5 1.5 0 0 1 14 6v5.25A1.75 1.75 0 0 1 12.25 13h-8.5A1.75 1.75 0 0 1 2 11.25V6a1.5 1.5 0 0 1 1.5-1.5Z"
22
+ stroke="currentColor"
23
+ strokeLinejoin="round"
24
+ strokeWidth="1.5"
25
+ />
26
+ <path
27
+ d="M2.25 7.5h11.5M7 7.5v1h2v-1"
28
+ stroke="currentColor"
29
+ strokeLinecap="round"
30
+ strokeWidth="1.5"
31
+ />
32
+ </svg>
33
+ );
34
+ }
35
+
36
+ function PersonalScheduleIcon(props: SVGProps<SVGSVGElement>) {
37
+ return (
38
+ <svg aria-hidden="true" fill="none" viewBox="0 0 16 16" {...props}>
39
+ <path
40
+ d="M2.75 7.25 8 3l5.25 4.25"
41
+ stroke="currentColor"
42
+ strokeLinecap="round"
43
+ strokeLinejoin="round"
44
+ strokeWidth="1.5"
45
+ />
46
+ <path
47
+ d="M4.25 6.75v5A1.25 1.25 0 0 0 5.5 13h5a1.25 1.25 0 0 0 1.25-1.25v-5"
48
+ stroke="currentColor"
49
+ strokeLinejoin="round"
50
+ strokeWidth="1.5"
51
+ />
52
+ <path
53
+ d="M6.5 13V9.75h3V13"
54
+ stroke="currentColor"
55
+ strokeLinejoin="round"
56
+ strokeWidth="1.5"
57
+ />
58
+ </svg>
59
+ );
60
+ }
61
+
62
+ function MeetingScheduleIcon(props: SVGProps<SVGSVGElement>) {
63
+ return (
64
+ <svg aria-hidden="true" fill="none" viewBox="0 0 16 16" {...props}>
65
+ <path
66
+ d="M5.25 7a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5ZM10.75 7a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z"
67
+ stroke="currentColor"
68
+ strokeWidth="1.5"
69
+ />
70
+ <path
71
+ d="M2.75 12.5v-.75A2.75 2.75 0 0 1 5.5 9h.25M13.25 12.5v-.75A2.75 2.75 0 0 0 10.5 9h-.25M6.75 10.5h2.5"
72
+ stroke="currentColor"
73
+ strokeLinecap="round"
74
+ strokeWidth="1.5"
75
+ />
76
+ </svg>
77
+ );
78
+ }
79
+
80
+ function AutoScheduleMark(props: SVGProps<SVGSVGElement>) {
81
+ return (
82
+ <svg aria-hidden="true" fill="none" viewBox="0 0 10 10" {...props}>
83
+ <path
84
+ d="M5.75 1.25 2.5 5.4h2.35l-.6 3.35L7.5 4.6H5.15l.6-3.35Z"
85
+ fill="currentColor"
86
+ />
87
+ </svg>
88
+ );
89
+ }
90
+
91
+ const SCHEDULE_BADGE_CONFIG = {
92
+ meeting_hours: {
93
+ className:
94
+ 'border-dynamic-orange/35 bg-dynamic-orange/10 text-dynamic-orange',
95
+ Icon: MeetingScheduleIcon,
96
+ },
97
+ personal_hours: {
98
+ className: 'border-dynamic-green/35 bg-dynamic-green/10 text-dynamic-green',
99
+ Icon: PersonalScheduleIcon,
100
+ },
101
+ work_hours: {
102
+ className: 'border-dynamic-blue/35 bg-dynamic-blue/10 text-dynamic-blue',
103
+ Icon: WorkScheduleIcon,
104
+ },
105
+ } satisfies Record<
106
+ CalendarHoursType,
107
+ {
108
+ className: string;
109
+ Icon: (props: SVGProps<SVGSVGElement>) => ReactElement;
110
+ }
111
+ >;
112
+
113
+ function getScheduleBadgeConfig(calendarHours: CalendarHoursType | null) {
114
+ return calendarHours
115
+ ? SCHEDULE_BADGE_CONFIG[calendarHours]
116
+ : {
117
+ className:
118
+ 'border-dynamic-gray/30 bg-dynamic-gray/10 text-dynamic-gray',
119
+ Icon: WorkScheduleIcon,
120
+ };
121
+ }
122
+
123
+ export function TaskSchedulingBadge({
124
+ autoSchedule,
125
+ calendarHours,
126
+ isSplittable,
127
+ labels,
128
+ maxSplitDurationMinutes,
129
+ minSplitDurationMinutes,
130
+ onElement,
131
+ totalDuration,
132
+ }: {
133
+ autoSchedule?: boolean | null;
134
+ calendarHours?: CalendarHoursType | null;
135
+ isSplittable?: boolean | null;
136
+ labels: TaskSchedulingBadgeTitleLabels;
137
+ maxSplitDurationMinutes?: number | null;
138
+ minSplitDurationMinutes?: number | null;
139
+ onElement?: (element: HTMLElement | null) => void;
140
+ totalDuration?: number | null;
141
+ }) {
142
+ const durationLabel = formatTaskDurationLabel(totalDuration ?? null);
143
+ if (!durationLabel) return null;
144
+
145
+ const scheduleBadgeConfig = getScheduleBadgeConfig(calendarHours ?? null);
146
+ const ScheduleIcon = scheduleBadgeConfig.Icon;
147
+ const schedulingTitle = formatTaskSchedulingBadgeTitle({
148
+ autoSchedule,
149
+ calendarHours,
150
+ durationLabel,
151
+ isSplittable,
152
+ labels,
153
+ maxSplitDurationMinutes,
154
+ minSplitDurationMinutes,
155
+ });
156
+
157
+ return (
158
+ <Badge
159
+ variant="secondary"
160
+ className={cn(
161
+ 'h-5 shrink-0 border px-1.5 font-medium text-[10px]',
162
+ scheduleBadgeConfig.className
163
+ )}
164
+ title={schedulingTitle}
165
+ ref={(element) => onElement?.(element as HTMLElement | null)}
166
+ >
167
+ <ScheduleIcon className="h-2.5 w-2.5" />
168
+ {durationLabel}
169
+ {autoSchedule && (
170
+ <AutoScheduleMark className="-mr-0.5 h-2.5 w-2.5 opacity-80" />
171
+ )}
172
+ </Badge>
173
+ );
174
+ }
@@ -1,14 +1,5 @@
1
1
  'use client';
2
2
 
3
- import {
4
- listWorkspaceLabels,
5
- listWorkspaceMembers,
6
- } from '@tuturuuu/internal-api';
7
- import {
8
- getCurrentUserTask,
9
- listWorkspaceTaskProjectsByIds,
10
- resolveTaskProjectWorkspaceId,
11
- } from '@tuturuuu/internal-api/tasks';
12
3
  import type { WorkspaceProductTier } from '@tuturuuu/types';
13
4
  import type { Task } from '@tuturuuu/types/primitives/Task';
14
5
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
@@ -31,6 +22,20 @@ import { useOptionalWorkspacePresenceContext } from './workspace-presence-provid
31
22
 
32
23
  export type { PendingRelationship, PendingRelationshipType };
33
24
 
25
+ type WorkspaceLabelSummary = {
26
+ id: string;
27
+ name: string | null;
28
+ color: string | null;
29
+ created_at: string | null;
30
+ };
31
+
32
+ type WorkspaceMemberSummary = {
33
+ id: string;
34
+ user_id?: string | null;
35
+ display_name?: string | null;
36
+ avatar_url?: string | null;
37
+ };
38
+
34
39
  interface TaskDialogState {
35
40
  isOpen: boolean;
36
41
  task?: Task;
@@ -356,6 +361,10 @@ export function TaskDialogProvider({
356
361
  | undefined;
357
362
 
358
363
  try {
364
+ const { getCurrentUserTask } = await import(
365
+ '@tuturuuu/internal-api/tasks'
366
+ );
367
+
359
368
  response = await getCurrentUserTask(taskId, {
360
369
  fetch: (input, init) =>
361
370
  fetch(new URL(String(input), window.location.origin).toString(), {
@@ -526,6 +535,9 @@ export function TaskDialogProvider({
526
535
  assignee_ids?: string[];
527
536
  project_ids?: string[];
528
537
  }) => {
538
+ const { resolveTaskProjectWorkspaceId } = await import(
539
+ '@tuturuuu/internal-api/tasks'
540
+ );
529
541
  const workspaceId = await resolveTaskProjectWorkspaceId({
530
542
  boardId: draft.board_id ?? undefined,
531
543
  projectIds: draft.project_ids,
@@ -543,12 +555,15 @@ export function TaskDialogProvider({
543
555
  created_at: string;
544
556
  }> = [];
545
557
  if (draft.label_ids && draft.label_ids.length > 0) {
558
+ const { listWorkspaceLabels } = await import(
559
+ '@tuturuuu/internal-api/tasks'
560
+ );
546
561
  const data = await listWorkspaceLabels(workspaceId);
547
562
  labels = data
548
- .filter((label: (typeof data)[number]) =>
563
+ .filter((label: WorkspaceLabelSummary) =>
549
564
  draft.label_ids?.includes(label.id)
550
565
  )
551
- .map((l: (typeof data)[number]) => ({
566
+ .map((l: WorkspaceLabelSummary) => ({
552
567
  id: l.id,
553
568
  name: l.name ?? '',
554
569
  color: l.color ?? '',
@@ -564,12 +579,15 @@ export function TaskDialogProvider({
564
579
  avatar_url?: string | null;
565
580
  }> = [];
566
581
  if (draft.assignee_ids && draft.assignee_ids.length > 0) {
582
+ const { listWorkspaceMembers } = await import(
583
+ '@tuturuuu/internal-api/workspaces'
584
+ );
567
585
  const data = await listWorkspaceMembers(workspaceId);
568
586
  assignees = data
569
- .filter((user: (typeof data)[number]) =>
587
+ .filter((user: WorkspaceMemberSummary) =>
570
588
  Boolean(user.user_id && draft.assignee_ids?.includes(user.user_id))
571
589
  )
572
- .map((u: (typeof data)[number]) => ({
590
+ .map((u: WorkspaceMemberSummary) => ({
573
591
  id: u.id,
574
592
  user_id: u.user_id || u.id,
575
593
  display_name: u.display_name,
@@ -584,6 +602,9 @@ export function TaskDialogProvider({
584
602
  status: string | null;
585
603
  }> = [];
586
604
  if (draft.project_ids && draft.project_ids.length > 0) {
605
+ const { listWorkspaceTaskProjectsByIds } = await import(
606
+ '@tuturuuu/internal-api/tasks'
607
+ );
587
608
  const workspaceProjects = await listWorkspaceTaskProjectsByIds(
588
609
  workspaceId,
589
610
  draft.project_ids