@tuturuuu/ui 0.2.0 → 0.3.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.
- package/CHANGELOG.md +53 -0
- package/package.json +79 -67
- package/src/components/ui/__tests__/avatar.test.tsx +8 -5
- package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
- package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
- package/src/components/ui/chart.test.tsx +29 -0
- package/src/components/ui/chart.tsx +12 -3
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
- package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
- package/src/components/ui/custom/common-footer.tsx +16 -1
- package/src/components/ui/custom/production-indicator.tsx +1 -1
- package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
- package/src/components/ui/custom/settings/task-settings.tsx +18 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
- package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
- package/src/components/ui/custom/sidebar-context.tsx +61 -61
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
- package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
- package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
- package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
- package/src/components/ui/custom/workspace-select.tsx +33 -12
- package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
- package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
- package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
- package/src/components/ui/finance/invoices/hooks.ts +75 -20
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
- package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
- package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
- package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
- package/src/components/ui/finance/invoices/utils.test.ts +50 -0
- package/src/components/ui/finance/invoices/utils.ts +75 -17
- package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
- package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-types.ts +2 -0
- package/src/components/ui/finance/transactions/form.test.tsx +43 -0
- package/src/components/ui/finance/transactions/form.tsx +60 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
- package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
- package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
- package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
- package/src/components/ui/legacy/meet/page.test.ts +180 -0
- package/src/components/ui/legacy/meet/page.tsx +87 -39
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
- package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
- package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
- package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
- package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
- package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
- package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
- package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
- package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
- package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
- package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
- package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
- package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
- package/src/hooks/use-calendar-sync.tsx +22 -277
- package/src/hooks/use-calendar.tsx +95 -525
- package/src/hooks/use-task-actions.ts +43 -117
- package/src/hooks/use-user-config.ts +1 -1
- package/src/hooks/use-workspace-config.ts +6 -2
- package/src/hooks/use-workspace-presence.ts +1 -1
- 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
|
|
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
|
-
<
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
-
{
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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:
|
|
563
|
+
.filter((label: WorkspaceLabelSummary) =>
|
|
549
564
|
draft.label_ids?.includes(label.id)
|
|
550
565
|
)
|
|
551
|
-
.map((l:
|
|
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:
|
|
587
|
+
.filter((user: WorkspaceMemberSummary) =>
|
|
570
588
|
Boolean(user.user_id && draft.assignee_ids?.includes(user.user_id))
|
|
571
589
|
)
|
|
572
|
-
.map((u:
|
|
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
|