@tuturuuu/ui 0.1.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 +71 -0
- package/package.json +82 -70
- 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/chat/chat-agent-details-external-thread-panel.test.tsx +43 -13
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +138 -74
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +60 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +13 -5
- package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
- package/src/components/ui/chat/chat-sidebar-panel.tsx +13 -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/legacy/meet/planId/page.tsx +10 -4
- package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +203 -6
- package/src/components/ui/text-editor/task-mention-chip.tsx +29 -7
- 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/__tests__/use-task-realtime-sync.test.tsx +37 -9
- 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-realtime-sync.ts +89 -70
- 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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { CalendarHoursType } from '@tuturuuu/types/primitives/Task';
|
|
2
|
+
|
|
3
|
+
export function taskDurationHoursToMinutes(durationHours: number | null) {
|
|
4
|
+
if (!durationHours || durationHours <= 0) return 0;
|
|
5
|
+
return Math.round(durationHours * 60);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function taskDurationMinutesToHours(durationMinutes: number) {
|
|
9
|
+
if (durationMinutes <= 0) return null;
|
|
10
|
+
return Number((durationMinutes / 60).toFixed(2));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatTaskDurationLabel(durationHours: number | null) {
|
|
14
|
+
const totalMinutes = taskDurationHoursToMinutes(durationHours);
|
|
15
|
+
if (totalMinutes <= 0) return null;
|
|
16
|
+
|
|
17
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
18
|
+
const minutes = totalMinutes % 60;
|
|
19
|
+
|
|
20
|
+
if (hours === 0) return `${minutes}m`;
|
|
21
|
+
if (minutes === 0) return `${hours}h`;
|
|
22
|
+
return `${hours}h ${minutes}m`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TaskSchedulingBadgeTitleLabels {
|
|
26
|
+
autoSchedule: string;
|
|
27
|
+
estimatedDuration: string;
|
|
28
|
+
meetingHours: string;
|
|
29
|
+
personalHours: string;
|
|
30
|
+
splittable: string;
|
|
31
|
+
workHours: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getTaskSchedulingHourTypeLabel(
|
|
35
|
+
calendarHours: CalendarHoursType | null | undefined,
|
|
36
|
+
labels: Pick<
|
|
37
|
+
TaskSchedulingBadgeTitleLabels,
|
|
38
|
+
'meetingHours' | 'personalHours' | 'workHours'
|
|
39
|
+
>
|
|
40
|
+
) {
|
|
41
|
+
switch (calendarHours) {
|
|
42
|
+
case 'work_hours':
|
|
43
|
+
return labels.workHours;
|
|
44
|
+
case 'meeting_hours':
|
|
45
|
+
return labels.meetingHours;
|
|
46
|
+
case 'personal_hours':
|
|
47
|
+
return labels.personalHours;
|
|
48
|
+
default:
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatSplitMinutesRangeLabel(
|
|
54
|
+
minSplitDurationMinutes: number | null | undefined,
|
|
55
|
+
maxSplitDurationMinutes: number | null | undefined
|
|
56
|
+
) {
|
|
57
|
+
if (!minSplitDurationMinutes || !maxSplitDurationMinutes) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const minLabel = formatTaskDurationLabel(minSplitDurationMinutes / 60);
|
|
62
|
+
const maxLabel = formatTaskDurationLabel(maxSplitDurationMinutes / 60);
|
|
63
|
+
if (!minLabel || !maxLabel) return null;
|
|
64
|
+
|
|
65
|
+
return `${minLabel}-${maxLabel}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function formatTaskSchedulingBadgeTitle({
|
|
69
|
+
autoSchedule,
|
|
70
|
+
calendarHours,
|
|
71
|
+
durationLabel,
|
|
72
|
+
isSplittable,
|
|
73
|
+
labels,
|
|
74
|
+
maxSplitDurationMinutes,
|
|
75
|
+
minSplitDurationMinutes,
|
|
76
|
+
}: {
|
|
77
|
+
autoSchedule?: boolean | null;
|
|
78
|
+
calendarHours?: CalendarHoursType | null;
|
|
79
|
+
durationLabel: string;
|
|
80
|
+
isSplittable?: boolean | null;
|
|
81
|
+
labels: TaskSchedulingBadgeTitleLabels;
|
|
82
|
+
maxSplitDurationMinutes?: number | null;
|
|
83
|
+
minSplitDurationMinutes?: number | null;
|
|
84
|
+
}) {
|
|
85
|
+
const titleParts = [`${labels.estimatedDuration}: ${durationLabel}`];
|
|
86
|
+
const hourTypeLabel = getTaskSchedulingHourTypeLabel(calendarHours, labels);
|
|
87
|
+
|
|
88
|
+
if (hourTypeLabel) {
|
|
89
|
+
titleParts.push(hourTypeLabel);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (isSplittable) {
|
|
93
|
+
const splitRangeLabel = formatSplitMinutesRangeLabel(
|
|
94
|
+
minSplitDurationMinutes,
|
|
95
|
+
maxSplitDurationMinutes
|
|
96
|
+
);
|
|
97
|
+
titleParts.push(
|
|
98
|
+
splitRangeLabel
|
|
99
|
+
? `${labels.splittable}: ${splitRangeLabel}`
|
|
100
|
+
: labels.splittable
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (autoSchedule) {
|
|
105
|
+
titleParts.push(labels.autoSchedule);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return titleParts.join(' | ');
|
|
109
|
+
}
|
|
@@ -2,6 +2,7 @@ import { BoardClient } from '@tuturuuu/ui/tu-do/shared/board-client';
|
|
|
2
2
|
import { getCurrentUser } from '@tuturuuu/utils/user-helper';
|
|
3
3
|
import { getWorkspace } from '@tuturuuu/utils/workspace-helper';
|
|
4
4
|
import { notFound, redirect } from 'next/navigation';
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
params: Promise<{
|
|
@@ -10,6 +11,7 @@ interface Props {
|
|
|
10
11
|
}>;
|
|
11
12
|
/** Route prefix for tasks URLs. Defaults to '/tasks' (web app). Set to '' for satellite apps. */
|
|
12
13
|
routePrefix?: string;
|
|
14
|
+
idleBottomIsland?: ReactNode;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -18,6 +20,7 @@ interface Props {
|
|
|
18
20
|
* Used by both apps/web and apps/tasks.
|
|
19
21
|
*/
|
|
20
22
|
export default async function TaskBoardServerPage({
|
|
23
|
+
idleBottomIsland,
|
|
21
24
|
params,
|
|
22
25
|
routePrefix = '/tasks',
|
|
23
26
|
}: Props) {
|
|
@@ -36,6 +39,7 @@ export default async function TaskBoardServerPage({
|
|
|
36
39
|
workspaceTier={(workspace as any)?.tier ?? null}
|
|
37
40
|
currentUserId={user.id}
|
|
38
41
|
routePrefix={routePrefix}
|
|
42
|
+
idleBottomIsland={idleBottomIsland}
|
|
39
43
|
/>
|
|
40
44
|
);
|
|
41
45
|
}
|
|
@@ -5,8 +5,11 @@ import { Checkbox } from '@tuturuuu/ui/checkbox';
|
|
|
5
5
|
import { cn } from '@tuturuuu/utils/format';
|
|
6
6
|
import { isTaskBoardResolvedStatus } from '@tuturuuu/utils/task-list-status';
|
|
7
7
|
import { type MouseEvent, memo } from 'react';
|
|
8
|
-
import { getListColorClasses } from '../../../utils/taskColorUtils';
|
|
9
8
|
import { isOverdue } from '../../../utils/taskDateUtils';
|
|
9
|
+
import {
|
|
10
|
+
getTaskCardCheckboxToneClasses,
|
|
11
|
+
TASK_CARD_OVERDUE_CHECKBOX_TONE_CLASSES,
|
|
12
|
+
} from './task-card-checkbox-style';
|
|
10
13
|
|
|
11
14
|
interface TaskCardCheckboxProps {
|
|
12
15
|
task: Task;
|
|
@@ -31,11 +34,11 @@ export const TaskCardCheckbox = memo(function TaskCardCheckbox({
|
|
|
31
34
|
'h-4 w-4 flex-none transition-all duration-200',
|
|
32
35
|
'data-[state=checked]:border-dynamic-green/70 data-[state=checked]:bg-dynamic-green/70',
|
|
33
36
|
'hover:scale-110 hover:border-primary/50',
|
|
34
|
-
|
|
37
|
+
getTaskCardCheckboxToneClasses(taskList?.color as SupportedColor),
|
|
35
38
|
taskIsOverdue &&
|
|
36
39
|
!task.closed_at &&
|
|
37
40
|
!isInResolvedList &&
|
|
38
|
-
|
|
41
|
+
TASK_CARD_OVERDUE_CHECKBOX_TONE_CLASSES
|
|
39
42
|
)}
|
|
40
43
|
disabled={isLoading}
|
|
41
44
|
onCheckedChange={onToggle}
|
|
@@ -3,13 +3,18 @@ import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
|
3
3
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
4
4
|
import { Badge } from '@tuturuuu/ui/badge';
|
|
5
5
|
import { useCalendarPreferences } from '@tuturuuu/ui/hooks/use-calendar-preferences';
|
|
6
|
+
import { useUserBooleanConfig } from '@tuturuuu/ui/hooks/use-user-config';
|
|
6
7
|
import { cn } from '@tuturuuu/utils/format';
|
|
7
|
-
import { isTaskBoardResolvedStatus } from '@tuturuuu/utils/task-list-status';
|
|
8
8
|
import { getTimeFormatPattern } from '@tuturuuu/utils/time-helper';
|
|
9
9
|
import { format } from 'date-fns';
|
|
10
10
|
import { enUS, vi } from 'date-fns/locale';
|
|
11
11
|
import { useLocale, useTranslations } from 'next-intl';
|
|
12
12
|
import { memo } from 'react';
|
|
13
|
+
import {
|
|
14
|
+
shouldShowTaskDueDate,
|
|
15
|
+
shouldShowTaskStartDate,
|
|
16
|
+
TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID,
|
|
17
|
+
} from '../../../shared/task-due-date-visibility';
|
|
13
18
|
import {
|
|
14
19
|
formatSmartDate,
|
|
15
20
|
isFutureDate,
|
|
@@ -29,25 +34,37 @@ export const TaskCardDates = memo(function TaskCardDates({
|
|
|
29
34
|
const locale = useLocale();
|
|
30
35
|
const dateLocale = locale === 'vi' ? vi : enUS;
|
|
31
36
|
const { timeFormat } = useCalendarPreferences();
|
|
37
|
+
const { value: showReviewDueDates } = useUserBooleanConfig(
|
|
38
|
+
TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID,
|
|
39
|
+
false
|
|
40
|
+
);
|
|
32
41
|
const timePattern = getTimeFormatPattern(timeFormat);
|
|
33
42
|
const startDate = task.start_date ? new Date(task.start_date) : null;
|
|
34
43
|
const endDate = task.end_date ? new Date(task.end_date) : null;
|
|
35
44
|
const taskIsOverdue = isOverdue(endDate);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
const shouldRenderStartDate = shouldShowTaskStartDate({
|
|
46
|
+
completedAt: task.completed_at,
|
|
47
|
+
closedAt: task.closed_at,
|
|
48
|
+
listStatus: taskList?.status,
|
|
49
|
+
startDate: task.start_date,
|
|
50
|
+
});
|
|
51
|
+
const shouldRenderDueDate = shouldShowTaskDueDate({
|
|
52
|
+
completedAt: task.completed_at,
|
|
53
|
+
closedAt: task.closed_at,
|
|
54
|
+
dueDate: task.end_date,
|
|
55
|
+
listStatus: taskList?.status,
|
|
56
|
+
showReviewDueDates,
|
|
57
|
+
});
|
|
41
58
|
|
|
42
59
|
// Don't render if no dates
|
|
43
|
-
if (!
|
|
60
|
+
if (!shouldRenderStartDate && !shouldRenderDueDate) {
|
|
44
61
|
return null;
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
return (
|
|
48
65
|
<div className="mb-1 space-y-0.5 text-[10px] leading-snug">
|
|
49
66
|
{/* Show start only if in the future (hide historical start for visual simplicity) */}
|
|
50
|
-
{startDate && isFutureDate(startDate) && (
|
|
67
|
+
{shouldRenderStartDate && startDate && isFutureDate(startDate) && (
|
|
51
68
|
<div className="flex items-center gap-1 text-muted-foreground">
|
|
52
69
|
<Clock className="h-2.5 w-2.5 shrink-0" />
|
|
53
70
|
<span className="truncate">
|
|
@@ -65,7 +82,7 @@ export const TaskCardDates = memo(function TaskCardDates({
|
|
|
65
82
|
</span>
|
|
66
83
|
</div>
|
|
67
84
|
)}
|
|
68
|
-
{endDate && (
|
|
85
|
+
{shouldRenderDueDate && endDate && (
|
|
69
86
|
<div
|
|
70
87
|
className={cn(
|
|
71
88
|
'flex items-center gap-1',
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { SupportedColor } from '@tuturuuu/types/primitives/SupportedColors';
|
|
2
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
3
|
+
import { getListColorClasses } from '../../../utils/taskColorUtils';
|
|
4
|
+
|
|
5
|
+
const CHECKED_LIST_TONE_CLASSES: Record<SupportedColor, string> = {
|
|
6
|
+
BLUE: 'data-[state=checked]:border-dynamic-blue/70 data-[state=checked]:bg-dynamic-blue/20 data-[state=checked]:text-dynamic-blue',
|
|
7
|
+
CYAN: 'data-[state=checked]:border-dynamic-cyan/70 data-[state=checked]:bg-dynamic-cyan/20 data-[state=checked]:text-dynamic-cyan',
|
|
8
|
+
GRAY: 'data-[state=checked]:border-dynamic-gray/70 data-[state=checked]:bg-dynamic-gray/20 data-[state=checked]:text-foreground',
|
|
9
|
+
GREEN:
|
|
10
|
+
'data-[state=checked]:border-dynamic-green/70 data-[state=checked]:bg-dynamic-green/20 data-[state=checked]:text-dynamic-green',
|
|
11
|
+
INDIGO:
|
|
12
|
+
'data-[state=checked]:border-dynamic-indigo/70 data-[state=checked]:bg-dynamic-indigo/20 data-[state=checked]:text-dynamic-indigo',
|
|
13
|
+
ORANGE:
|
|
14
|
+
'data-[state=checked]:border-dynamic-orange/70 data-[state=checked]:bg-dynamic-orange/20 data-[state=checked]:text-dynamic-orange',
|
|
15
|
+
PINK: 'data-[state=checked]:border-dynamic-pink/70 data-[state=checked]:bg-dynamic-pink/20 data-[state=checked]:text-dynamic-pink',
|
|
16
|
+
PURPLE:
|
|
17
|
+
'data-[state=checked]:border-dynamic-purple/70 data-[state=checked]:bg-dynamic-purple/20 data-[state=checked]:text-dynamic-purple',
|
|
18
|
+
RED: 'data-[state=checked]:border-dynamic-red/70 data-[state=checked]:bg-dynamic-red/20 data-[state=checked]:text-dynamic-red',
|
|
19
|
+
YELLOW:
|
|
20
|
+
'data-[state=checked]:border-dynamic-yellow/70 data-[state=checked]:bg-dynamic-yellow/20 data-[state=checked]:text-dynamic-yellow',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const TASK_CARD_OVERDUE_CHECKBOX_TONE_CLASSES =
|
|
24
|
+
'border-dynamic-red/70 bg-dynamic-red/10 ring-1 ring-dynamic-red/20 data-[state=checked]:border-dynamic-red/70 data-[state=checked]:bg-dynamic-red/20 data-[state=checked]:text-dynamic-red';
|
|
25
|
+
|
|
26
|
+
export function getTaskCardCheckboxToneClasses(color?: SupportedColor | null) {
|
|
27
|
+
return getListColorClasses(color ?? 'GRAY');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getTaskCardSelectionCheckboxToneClasses(
|
|
31
|
+
color?: SupportedColor | null
|
|
32
|
+
) {
|
|
33
|
+
const resolvedColor = color ?? 'GRAY';
|
|
34
|
+
|
|
35
|
+
return cn(
|
|
36
|
+
getTaskCardCheckboxToneClasses(resolvedColor),
|
|
37
|
+
CHECKED_LIST_TONE_CLASSES[resolvedColor]
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -58,4 +58,47 @@ describe('areTaskCardPropsEqual', () => {
|
|
|
58
58
|
)
|
|
59
59
|
).toBe(false);
|
|
60
60
|
});
|
|
61
|
+
|
|
62
|
+
it('rerenders when personal scheduling duration changes', () => {
|
|
63
|
+
expect(
|
|
64
|
+
areTaskCardPropsEqual(
|
|
65
|
+
taskCardProps({ task: { ...task, total_duration: null } }),
|
|
66
|
+
taskCardProps({ task: { ...task, total_duration: 1.5 } })
|
|
67
|
+
)
|
|
68
|
+
).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('rerenders a selected card when the bulk selection grows', () => {
|
|
72
|
+
expect(
|
|
73
|
+
areTaskCardPropsEqual(
|
|
74
|
+
taskCardProps({
|
|
75
|
+
isMultiSelectMode: true,
|
|
76
|
+
isSelected: true,
|
|
77
|
+
selectedTasks: new Set(['task-1']),
|
|
78
|
+
}),
|
|
79
|
+
taskCardProps({
|
|
80
|
+
isMultiSelectMode: true,
|
|
81
|
+
isSelected: true,
|
|
82
|
+
selectedTasks: new Set(['task-1', 'task-2']),
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('rerenders a selected card when the bulk selection changes with the same size', () => {
|
|
89
|
+
expect(
|
|
90
|
+
areTaskCardPropsEqual(
|
|
91
|
+
taskCardProps({
|
|
92
|
+
isMultiSelectMode: true,
|
|
93
|
+
isSelected: true,
|
|
94
|
+
selectedTasks: new Set(['task-1', 'task-2']),
|
|
95
|
+
}),
|
|
96
|
+
taskCardProps({
|
|
97
|
+
isMultiSelectMode: true,
|
|
98
|
+
isSelected: true,
|
|
99
|
+
selectedTasks: new Set(['task-1', 'task-3']),
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
).toBe(false);
|
|
103
|
+
});
|
|
61
104
|
});
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import type { TaskCardProps } from './task-card';
|
|
2
2
|
|
|
3
|
+
function areSelectedTaskSetsEqual(
|
|
4
|
+
previousSelectedTasks?: Set<string>,
|
|
5
|
+
nextSelectedTasks?: Set<string>
|
|
6
|
+
) {
|
|
7
|
+
if (previousSelectedTasks === nextSelectedTasks) return true;
|
|
8
|
+
if (!previousSelectedTasks || !nextSelectedTasks) {
|
|
9
|
+
return (
|
|
10
|
+
(previousSelectedTasks?.size ?? 0) === (nextSelectedTasks?.size ?? 0)
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (previousSelectedTasks.size !== nextSelectedTasks.size) return false;
|
|
15
|
+
|
|
16
|
+
for (const taskId of previousSelectedTasks) {
|
|
17
|
+
if (!nextSelectedTasks.has(taskId)) return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
3
23
|
export function areTaskCardPropsEqual(
|
|
4
24
|
prev: TaskCardProps,
|
|
5
25
|
next: TaskCardProps
|
|
@@ -7,6 +27,13 @@ export function areTaskCardPropsEqual(
|
|
|
7
27
|
if (prev.isOverlay !== next.isOverlay) return false;
|
|
8
28
|
if (prev.isSelected !== next.isSelected) return false;
|
|
9
29
|
if (prev.isMultiSelectMode !== next.isMultiSelectMode) return false;
|
|
30
|
+
if (
|
|
31
|
+
(prev.isSelected || next.isSelected) &&
|
|
32
|
+
(prev.isMultiSelectMode || next.isMultiSelectMode) &&
|
|
33
|
+
!areSelectedTaskSetsEqual(prev.selectedTasks, next.selectedTasks)
|
|
34
|
+
) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
10
37
|
if (prev.boardId !== next.boardId) return false;
|
|
11
38
|
if (prev.workspaceId !== next.workspaceId) return false;
|
|
12
39
|
if (prev.dragDisabled !== next.dragDisabled) return false;
|
|
@@ -42,6 +69,12 @@ export function areTaskCardPropsEqual(
|
|
|
42
69
|
'start_date',
|
|
43
70
|
'completed_at',
|
|
44
71
|
'estimation_points',
|
|
72
|
+
'total_duration',
|
|
73
|
+
'is_splittable',
|
|
74
|
+
'min_split_duration_minutes',
|
|
75
|
+
'max_split_duration_minutes',
|
|
76
|
+
'calendar_hours',
|
|
77
|
+
'auto_schedule',
|
|
45
78
|
'relationship_summary',
|
|
46
79
|
'list_id',
|
|
47
80
|
];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { shouldRenderTaskCardCompletionCheckbox } from './task-card-completion-checkbox-visibility';
|
|
3
|
+
|
|
4
|
+
describe('shouldRenderTaskCardCompletionCheckbox', () => {
|
|
5
|
+
it('renders the done checkbox in normal task mode', () => {
|
|
6
|
+
expect(
|
|
7
|
+
shouldRenderTaskCardCompletionCheckbox({
|
|
8
|
+
isMultiSelectMode: false,
|
|
9
|
+
taskListStatus: 'active',
|
|
10
|
+
})
|
|
11
|
+
).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('hides the done checkbox in multi-select mode', () => {
|
|
15
|
+
expect(
|
|
16
|
+
shouldRenderTaskCardCompletionCheckbox({
|
|
17
|
+
isMultiSelectMode: true,
|
|
18
|
+
taskListStatus: 'active',
|
|
19
|
+
})
|
|
20
|
+
).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('keeps document-list completion checkboxes hidden', () => {
|
|
24
|
+
expect(
|
|
25
|
+
shouldRenderTaskCardCompletionCheckbox({
|
|
26
|
+
isMultiSelectMode: false,
|
|
27
|
+
taskListStatus: 'documents',
|
|
28
|
+
})
|
|
29
|
+
).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { getTaskCardSelectionCheckboxToneClasses } from './task-card-checkbox-style';
|
|
5
|
+
import { TaskCardIdentifierRow } from './task-card-identifier-row';
|
|
6
|
+
|
|
7
|
+
describe('TaskCardIdentifierRow', () => {
|
|
8
|
+
it('renders the selection checkbox before the external source and task identifier', () => {
|
|
9
|
+
const onSelect = vi.fn();
|
|
10
|
+
|
|
11
|
+
render(
|
|
12
|
+
<TaskCardIdentifierRow
|
|
13
|
+
externalSourceLabel="Upskii"
|
|
14
|
+
externalSourceTitle="Upskii / Roadmap / Review"
|
|
15
|
+
isMultiSelectMode
|
|
16
|
+
isPersonalExternalTask
|
|
17
|
+
isSelected={false}
|
|
18
|
+
onSelect={onSelect}
|
|
19
|
+
selectTaskLabel="Select Draft response"
|
|
20
|
+
selectionCheckboxClassName={getTaskCardSelectionCheckboxToneClasses(
|
|
21
|
+
'BLUE'
|
|
22
|
+
)}
|
|
23
|
+
taskListStatus="active"
|
|
24
|
+
ticketBadgeClassName="text-dynamic-blue"
|
|
25
|
+
ticketIdentifier="OH-167"
|
|
26
|
+
ticketTitle="Task OH-167"
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const source = screen.getByTestId('task-card-external-source');
|
|
31
|
+
const checkbox = screen.getByTestId('task-card-selection-checkbox');
|
|
32
|
+
const ticket = screen.getByTestId('task-card-ticket-identifier');
|
|
33
|
+
|
|
34
|
+
expect(checkbox.compareDocumentPosition(source)).toBe(
|
|
35
|
+
Node.DOCUMENT_POSITION_FOLLOWING
|
|
36
|
+
);
|
|
37
|
+
expect(source.compareDocumentPosition(ticket)).toBe(
|
|
38
|
+
Node.DOCUMENT_POSITION_FOLLOWING
|
|
39
|
+
);
|
|
40
|
+
expect(checkbox).toHaveClass('border-dynamic-blue/70');
|
|
41
|
+
expect(checkbox).toHaveClass('bg-dynamic-blue/5');
|
|
42
|
+
expect(checkbox.className).toContain(
|
|
43
|
+
'data-[state=checked]:border-dynamic-blue/70'
|
|
44
|
+
);
|
|
45
|
+
expect(checkbox).not.toHaveClass('absolute');
|
|
46
|
+
expect(checkbox).not.toHaveClass('bg-background/80');
|
|
47
|
+
|
|
48
|
+
fireEvent.click(checkbox);
|
|
49
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renders the selection checkbox before an internal task identifier', () => {
|
|
53
|
+
render(
|
|
54
|
+
<TaskCardIdentifierRow
|
|
55
|
+
externalSourceLabel="Source"
|
|
56
|
+
isMultiSelectMode
|
|
57
|
+
isPersonalExternalTask={false}
|
|
58
|
+
isSelected={false}
|
|
59
|
+
selectTaskLabel="Select task"
|
|
60
|
+
selectionCheckboxClassName={getTaskCardSelectionCheckboxToneClasses(
|
|
61
|
+
'GREEN'
|
|
62
|
+
)}
|
|
63
|
+
taskListStatus="active"
|
|
64
|
+
ticketIdentifier="OH-174"
|
|
65
|
+
ticketTitle="Task OH-174"
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const checkbox = screen.getByTestId('task-card-selection-checkbox');
|
|
70
|
+
const ticket = screen.getByTestId('task-card-ticket-identifier');
|
|
71
|
+
|
|
72
|
+
expect(checkbox.compareDocumentPosition(ticket)).toBe(
|
|
73
|
+
Node.DOCUMENT_POSITION_FOLLOWING
|
|
74
|
+
);
|
|
75
|
+
expect(screen.queryByTestId('task-card-external-source')).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('keeps the selection checkbox first when document lists omit the identifier', () => {
|
|
79
|
+
render(
|
|
80
|
+
<TaskCardIdentifierRow
|
|
81
|
+
externalSourceLabel="Exocorpse"
|
|
82
|
+
externalSourceTitle="Exocorpse / Web"
|
|
83
|
+
isMultiSelectMode
|
|
84
|
+
isPersonalExternalTask
|
|
85
|
+
isSelected={false}
|
|
86
|
+
selectTaskLabel="Select task"
|
|
87
|
+
selectionCheckboxClassName={getTaskCardSelectionCheckboxToneClasses(
|
|
88
|
+
'PURPLE'
|
|
89
|
+
)}
|
|
90
|
+
taskListStatus="documents"
|
|
91
|
+
ticketIdentifier="WEB-54"
|
|
92
|
+
ticketTitle="Task WEB-54"
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const checkbox = screen.getByTestId('task-card-selection-checkbox');
|
|
97
|
+
const source = screen.getByTestId('task-card-external-source');
|
|
98
|
+
|
|
99
|
+
expect(checkbox.compareDocumentPosition(source)).toBe(
|
|
100
|
+
Node.DOCUMENT_POSITION_FOLLOWING
|
|
101
|
+
);
|
|
102
|
+
expect(screen.queryByTestId('task-card-ticket-identifier')).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('omits the selector outside multi-select mode', () => {
|
|
106
|
+
render(
|
|
107
|
+
<TaskCardIdentifierRow
|
|
108
|
+
externalSourceLabel="Source"
|
|
109
|
+
isMultiSelectMode={false}
|
|
110
|
+
isPersonalExternalTask={false}
|
|
111
|
+
isSelected={false}
|
|
112
|
+
selectTaskLabel="Select task"
|
|
113
|
+
taskListStatus="active"
|
|
114
|
+
ticketIdentifier="OH-174"
|
|
115
|
+
ticketTitle="Task OH-174"
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(screen.queryByTestId('task-card-selection-checkbox')).toBeNull();
|
|
120
|
+
expect(screen.getByTestId('task-card-ticket-identifier')).toHaveTextContent(
|
|
121
|
+
'OH-174'
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ExternalLink } from '@tuturuuu/icons';
|
|
2
|
+
import { Badge } from '@tuturuuu/ui/badge';
|
|
3
|
+
import { Checkbox } from '@tuturuuu/ui/checkbox';
|
|
4
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
5
|
+
import type { MouseEvent } from 'react';
|
|
6
|
+
|
|
7
|
+
interface TaskCardIdentifierRowProps {
|
|
8
|
+
externalSourceLabel: string;
|
|
9
|
+
externalSourceTitle?: string;
|
|
10
|
+
isMultiSelectMode: boolean;
|
|
11
|
+
isPersonalExternalTask: boolean;
|
|
12
|
+
isSelected: boolean;
|
|
13
|
+
onSelect?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
14
|
+
selectTaskLabel: string;
|
|
15
|
+
selectionCheckboxClassName?: string;
|
|
16
|
+
taskListStatus?: string | null;
|
|
17
|
+
ticketBadgeClassName?: string;
|
|
18
|
+
ticketIdentifier: string | null;
|
|
19
|
+
ticketTitle: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function TaskCardIdentifierRow({
|
|
23
|
+
externalSourceLabel,
|
|
24
|
+
externalSourceTitle,
|
|
25
|
+
isMultiSelectMode,
|
|
26
|
+
isPersonalExternalTask,
|
|
27
|
+
isSelected,
|
|
28
|
+
onSelect,
|
|
29
|
+
selectTaskLabel,
|
|
30
|
+
selectionCheckboxClassName,
|
|
31
|
+
taskListStatus,
|
|
32
|
+
ticketBadgeClassName,
|
|
33
|
+
ticketIdentifier,
|
|
34
|
+
ticketTitle,
|
|
35
|
+
}: TaskCardIdentifierRowProps) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={cn(
|
|
39
|
+
'mb-1 flex min-w-0 flex-wrap items-center gap-1',
|
|
40
|
+
isPersonalExternalTask && 'gap-x-1.5'
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
{isMultiSelectMode && (
|
|
44
|
+
<Checkbox
|
|
45
|
+
checked={isSelected}
|
|
46
|
+
aria-label={selectTaskLabel}
|
|
47
|
+
data-testid="task-card-selection-checkbox"
|
|
48
|
+
className={cn(
|
|
49
|
+
'h-4 w-4 shrink-0 border-2 shadow-sm transition-all duration-200 hover:scale-110 hover:border-primary/50',
|
|
50
|
+
selectionCheckboxClassName
|
|
51
|
+
)}
|
|
52
|
+
onPointerDown={(event) => {
|
|
53
|
+
event.stopPropagation();
|
|
54
|
+
}}
|
|
55
|
+
onClick={(event) => {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
event.stopPropagation();
|
|
58
|
+
onSelect?.(event);
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
{isPersonalExternalTask && (
|
|
63
|
+
<Badge
|
|
64
|
+
variant="secondary"
|
|
65
|
+
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"
|
|
66
|
+
title={externalSourceTitle}
|
|
67
|
+
data-testid="task-card-external-source"
|
|
68
|
+
>
|
|
69
|
+
<ExternalLink className="h-2.5 w-2.5 shrink-0" />
|
|
70
|
+
<span className="truncate">{externalSourceLabel}</span>
|
|
71
|
+
</Badge>
|
|
72
|
+
)}
|
|
73
|
+
{taskListStatus !== 'documents' && ticketIdentifier && (
|
|
74
|
+
<Badge
|
|
75
|
+
variant="outline"
|
|
76
|
+
className={cn(
|
|
77
|
+
'w-fit px-1 py-0 font-mono text-[10px]',
|
|
78
|
+
ticketBadgeClassName
|
|
79
|
+
)}
|
|
80
|
+
title={ticketTitle}
|
|
81
|
+
data-testid="task-card-ticket-identifier"
|
|
82
|
+
>
|
|
83
|
+
{ticketIdentifier}
|
|
84
|
+
</Badge>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|