@tuturuuu/ui 0.13.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/biome.json +1 -1
- package/package.json +62 -62
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +12 -0
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +93 -6
- package/src/components/ui/custom/loading-statistic-card.tsx +18 -0
- package/src/components/ui/custom/notification-popover-client.tsx +5 -1
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.test.tsx +34 -0
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +106 -0
- package/src/components/ui/custom/settings/sidebar-settings.tsx +18 -6
- package/src/components/ui/custom/settings/task-settings.tsx +55 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +18 -12
- package/src/components/ui/custom/sidebar-context.tsx +58 -3
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +5 -3
- package/src/components/ui/custom/statistic-card.tsx +104 -0
- package/src/components/ui/custom/structure.test.tsx +29 -0
- package/src/components/ui/custom/structure.tsx +103 -88
- package/src/components/ui/finance/invoices/civil-month.ts +224 -0
- package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.test.tsx +49 -0
- package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.tsx +81 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +90 -2
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +40 -4
- package/src/components/ui/finance/invoices/pending-columns.test.ts +26 -0
- package/src/components/ui/finance/invoices/pending-columns.tsx +45 -14
- package/src/components/ui/finance/invoices/standard-invoice.test.ts +31 -0
- package/src/components/ui/finance/invoices/standard-invoice.tsx +33 -16
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +331 -0
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +53 -28
- package/src/components/ui/finance/invoices/utils.test.ts +84 -0
- package/src/components/ui/finance/invoices/utils.ts +56 -134
- package/src/components/ui/resizable.test.ts +15 -0
- package/src/components/ui/resizable.tsx +28 -6
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +4 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +62 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.test.ts +118 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/auto-scroll.ts +144 -70
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +48 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +50 -6
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +6 -0
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +4 -2
- package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +41 -3
- package/src/components/ui/tu-do/boards/boardId/menus/task-due-date-menu.tsx +31 -10
- package/src/components/ui/tu-do/boards/boardId/menus/task-priority-menu.tsx +7 -5
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +7 -5
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.test.ts +44 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +44 -30
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +54 -0
- package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +43 -1
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +33 -15
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +3 -3
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +74 -28
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +12 -10
- package/src/components/ui/tu-do/projects/components/project-grid-card.tsx +22 -6
- package/src/components/ui/tu-do/projects/components/project-list-item.tsx +24 -8
- package/src/components/ui/tu-do/projects/projectId/components/__tests__/tasks-tab-layout.test.tsx +6 -0
- package/src/components/ui/tu-do/projects/projectId/components/tasks-tab.tsx +3 -1
- package/src/components/ui/tu-do/projects/projectId/task-project-detail-page-client.tsx +11 -3
- package/src/components/ui/tu-do/projects/projectId/task-project-detail.tsx +2 -0
- package/src/components/ui/tu-do/projects/projectId/types.ts +1 -0
- package/src/components/ui/tu-do/projects/task-projects-client.tsx +45 -1
- package/src/components/ui/tu-do/projects/types.ts +4 -1
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +10 -0
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +7 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +87 -2
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +70 -0
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +27 -3
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +41 -3
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +54 -0
- package/src/components/ui/tu-do/shared/board-header.tsx +20 -0
- package/src/components/ui/tu-do/shared/board-views.tsx +33 -3
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +2 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +7 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-mutations.test.tsx +231 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +139 -68
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.test.tsx +68 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +17 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +45 -23
- package/src/components/ui/tu-do/shared/task-quick-create-target-list.ts +20 -0
- package/src/components/ui/tu-do/shared/task-row-actions-menu.test.tsx +138 -0
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +15 -9
- package/src/components/ui/tu-do/templates/client.tsx +97 -78
- package/src/components/ui/tu-do/templates/marketplace/client.tsx +171 -135
- package/src/components/ui/tu-do/templates/task-templates-hub.tsx +3 -0
- package/src/components/ui/tu-do/templates/templateId/client.tsx +24 -9
- package/src/components/ui/tu-do/templates/templateId/task-template-detail-page-client.tsx +8 -2
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -0
- package/src/hooks/__tests__/use-task-actions.test.tsx +14 -39
- package/src/hooks/task-actions-personal-external.ts +12 -6
- package/src/hooks/use-notifications.ts +14 -2
- package/src/hooks/use-user-config.ts +10 -2
- package/vitest.config.ts +4 -0
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
import type { Database } from '@tuturuuu/types';
|
|
2
|
+
import {
|
|
3
|
+
formatMonthLabel,
|
|
4
|
+
formatMonthValue,
|
|
5
|
+
getCoverageMonths,
|
|
6
|
+
getCurrentMonthStartDate,
|
|
7
|
+
getMonthStartDate,
|
|
8
|
+
parseLocalCalendarDate,
|
|
9
|
+
} from './civil-month';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
addMonthsToMonthValue,
|
|
13
|
+
formatCoverageRangeLabel,
|
|
14
|
+
formatMonthLabel,
|
|
15
|
+
formatMonthValue,
|
|
16
|
+
getCoverageEndMonthValue,
|
|
17
|
+
getCoverageMonths,
|
|
18
|
+
getCoverageValidUntilMonthValue,
|
|
19
|
+
getCurrentBillingDate,
|
|
20
|
+
getCurrentMonthStartDate,
|
|
21
|
+
getCurrentMonthValue,
|
|
22
|
+
getMonthStartDate,
|
|
23
|
+
MAX_PREPAID_MONTH_COUNT,
|
|
24
|
+
PREPAID_MONTH_OPTION_HORIZON,
|
|
25
|
+
parseLocalCalendarDate,
|
|
26
|
+
resolveBillingTimezone,
|
|
27
|
+
} from './civil-month';
|
|
2
28
|
|
|
3
29
|
type AttendanceRecord = {
|
|
4
30
|
status: string;
|
|
@@ -67,134 +93,25 @@ export const getLinkedFinanceCategorySelection = (
|
|
|
67
93
|
};
|
|
68
94
|
};
|
|
69
95
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
export const PREPAID_MONTH_OPTION_HORIZON = 12;
|
|
74
|
-
|
|
75
|
-
export const parseLocalCalendarDate = (
|
|
76
|
-
value: string | Date | null | undefined
|
|
77
|
-
): Date => {
|
|
78
|
-
if (!value) {
|
|
79
|
-
return new Date(Number.NaN);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (value instanceof Date) {
|
|
83
|
-
return new Date(value.getTime());
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const monthMatch = MONTH_VALUE_PATTERN.exec(value);
|
|
87
|
-
if (monthMatch) {
|
|
88
|
-
const [, year, month] = monthMatch;
|
|
89
|
-
return new Date(Number(year), Number(month) - 1, 1);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const dateMatch = DATE_VALUE_PATTERN.exec(value);
|
|
93
|
-
if (dateMatch) {
|
|
94
|
-
const [, year, month, day] = dateMatch;
|
|
95
|
-
return new Date(Number(year), Number(month) - 1, Number(day));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return new Date(value);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export const getMonthStartDate = (
|
|
102
|
-
value: string | Date | null | undefined
|
|
103
|
-
): Date => {
|
|
104
|
-
const date = parseLocalCalendarDate(value);
|
|
105
|
-
if (Number.isNaN(date.getTime())) {
|
|
106
|
-
return date;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
export const formatMonthValue = (date: Date): string =>
|
|
113
|
-
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
114
|
-
|
|
115
|
-
export const formatMonthLabel = (month: string, locale: string): string => {
|
|
116
|
-
const date = getMonthStartDate(month);
|
|
117
|
-
if (Number.isNaN(date.getTime())) {
|
|
118
|
-
return month;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return date.toLocaleDateString(locale, {
|
|
122
|
-
year: 'numeric',
|
|
123
|
-
month: 'long',
|
|
124
|
-
});
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
export const addMonthsToMonthValue = (
|
|
128
|
-
month: string,
|
|
129
|
-
monthOffset: number
|
|
130
|
-
): string => {
|
|
131
|
-
const date = getMonthStartDate(month);
|
|
132
|
-
if (Number.isNaN(date.getTime())) return month;
|
|
133
|
-
|
|
134
|
-
date.setMonth(date.getMonth() + monthOffset);
|
|
135
|
-
return formatMonthValue(date);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
export const normalizePrepaidMonthCount = (
|
|
139
|
-
value: number | null | undefined
|
|
140
|
-
): number => {
|
|
141
|
-
if (!Number.isFinite(value) || !value) return 1;
|
|
142
|
-
|
|
143
|
-
return Math.min(MAX_PREPAID_MONTH_COUNT, Math.max(1, Math.trunc(value)));
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
export const getCoverageMonths = (
|
|
147
|
-
selectedMonth: string,
|
|
148
|
-
prepaidMonthCount = 1
|
|
149
|
-
): string[] => {
|
|
150
|
-
const monthCount = normalizePrepaidMonthCount(prepaidMonthCount);
|
|
151
|
-
const startMonth = getMonthStartDate(selectedMonth);
|
|
152
|
-
if (Number.isNaN(startMonth.getTime())) return [];
|
|
153
|
-
|
|
154
|
-
return Array.from({ length: monthCount }, (_, index) => {
|
|
155
|
-
const date = new Date(startMonth);
|
|
156
|
-
date.setMonth(date.getMonth() + index);
|
|
157
|
-
return formatMonthValue(date);
|
|
158
|
-
});
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
export const getCoverageEndMonthValue = (
|
|
162
|
-
selectedMonth: string,
|
|
163
|
-
prepaidMonthCount = 1
|
|
164
|
-
): string => {
|
|
165
|
-
const coverageMonths = getCoverageMonths(selectedMonth, prepaidMonthCount);
|
|
166
|
-
return coverageMonths[coverageMonths.length - 1] ?? selectedMonth;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
export const getCoverageValidUntilMonthValue = (
|
|
170
|
-
selectedMonth: string,
|
|
171
|
-
prepaidMonthCount = 1
|
|
172
|
-
): string => addMonthsToMonthValue(selectedMonth, prepaidMonthCount);
|
|
173
|
-
|
|
174
|
-
export const formatCoverageRangeLabel = ({
|
|
175
|
-
locale,
|
|
176
|
-
prepaidMonthCount,
|
|
177
|
-
selectedMonth,
|
|
96
|
+
export const resolveSubscriptionInvoiceCategoryId = ({
|
|
97
|
+
defaultCategoryId,
|
|
98
|
+
items,
|
|
178
99
|
}: {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const lastMonth = coverageMonths[coverageMonths.length - 1];
|
|
100
|
+
defaultCategoryId?: string | null;
|
|
101
|
+
items: FinanceCategoryLinkedItem[];
|
|
102
|
+
}) => {
|
|
103
|
+
const linkedFinanceCategorySelection =
|
|
104
|
+
getLinkedFinanceCategorySelection(items);
|
|
105
|
+
|
|
106
|
+
if (linkedFinanceCategorySelection.hasSingleCategory) {
|
|
107
|
+
return linkedFinanceCategorySelection.categoryId ?? '';
|
|
108
|
+
}
|
|
189
109
|
|
|
190
|
-
if (
|
|
191
|
-
return
|
|
110
|
+
if (linkedFinanceCategorySelection.hasMixedCategories) {
|
|
111
|
+
return '';
|
|
192
112
|
}
|
|
193
113
|
|
|
194
|
-
return
|
|
195
|
-
lastMonth,
|
|
196
|
-
locale
|
|
197
|
-
)}`;
|
|
114
|
+
return defaultCategoryId ?? '';
|
|
198
115
|
};
|
|
199
116
|
|
|
200
117
|
const getComparableTimestamp = (value: string | null | undefined): number => {
|
|
@@ -532,7 +449,6 @@ export const getSessionsUntilMonth = (
|
|
|
532
449
|
}
|
|
533
450
|
};
|
|
534
451
|
|
|
535
|
-
/** Date range (earliest start, latest end) for selected groups. */
|
|
536
452
|
export type GroupsDateRange = {
|
|
537
453
|
earliestStart: Date | null;
|
|
538
454
|
latestEnd: Date | null;
|
|
@@ -583,17 +499,20 @@ export const getAvailableMonths = (
|
|
|
583
499
|
latestInvoices: SubscriptionCoverageInvoice[],
|
|
584
500
|
locale: string,
|
|
585
501
|
selectedMonthFallback: string | null = null,
|
|
586
|
-
futureMonthHorizon = 0
|
|
502
|
+
futureMonthHorizon = 0,
|
|
503
|
+
options: {
|
|
504
|
+
now?: Date;
|
|
505
|
+
workspaceTimezone?: string | null;
|
|
506
|
+
} = {}
|
|
587
507
|
): AvailableMonthOption[] => {
|
|
588
508
|
if (groupIds.length === 0) return [];
|
|
589
509
|
const { earliestStart, latestEnd } = getGroupsDateRange(userGroups, groupIds);
|
|
590
510
|
if (!earliestStart) return [];
|
|
591
511
|
|
|
592
|
-
const currentMonthStart = (
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
})();
|
|
512
|
+
const currentMonthStart = getCurrentMonthStartDate(
|
|
513
|
+
options.workspaceTimezone,
|
|
514
|
+
options.now
|
|
515
|
+
);
|
|
597
516
|
const futureHorizonEnd = new Date(currentMonthStart);
|
|
598
517
|
futureHorizonEnd.setMonth(
|
|
599
518
|
futureHorizonEnd.getMonth() + Math.max(0, futureMonthHorizon)
|
|
@@ -648,6 +567,7 @@ export const getBillableQuantityForGroupRange = ({
|
|
|
648
567
|
now = new Date(),
|
|
649
568
|
prepaidMonthCount = 1,
|
|
650
569
|
selectedMonth,
|
|
570
|
+
workspaceTimezone,
|
|
651
571
|
useAttendanceBased,
|
|
652
572
|
userAttendance,
|
|
653
573
|
userGroups,
|
|
@@ -657,11 +577,12 @@ export const getBillableQuantityForGroupRange = ({
|
|
|
657
577
|
now?: Date;
|
|
658
578
|
prepaidMonthCount?: number;
|
|
659
579
|
selectedMonth: string;
|
|
580
|
+
workspaceTimezone?: string | null;
|
|
660
581
|
useAttendanceBased: boolean;
|
|
661
582
|
userAttendance: AttendanceRecord[];
|
|
662
583
|
userGroups: UserGroup[];
|
|
663
584
|
}): number => {
|
|
664
|
-
const currentMonthStart =
|
|
585
|
+
const currentMonthStart = getCurrentMonthStartDate(workspaceTimezone, now);
|
|
665
586
|
const coverageMonths = getCoverageMonths(selectedMonth, prepaidMonthCount);
|
|
666
587
|
|
|
667
588
|
return coverageMonths.reduce((total, month) => {
|
|
@@ -708,6 +629,7 @@ export const getBillableQuantityMapForGroupsRange = ({
|
|
|
708
629
|
now,
|
|
709
630
|
prepaidMonthCount = 1,
|
|
710
631
|
selectedMonth,
|
|
632
|
+
workspaceTimezone,
|
|
711
633
|
useAttendanceBased,
|
|
712
634
|
userAttendance,
|
|
713
635
|
userGroups,
|
|
@@ -717,6 +639,7 @@ export const getBillableQuantityMapForGroupsRange = ({
|
|
|
717
639
|
now?: Date;
|
|
718
640
|
prepaidMonthCount?: number;
|
|
719
641
|
selectedMonth: string;
|
|
642
|
+
workspaceTimezone?: string | null;
|
|
720
643
|
useAttendanceBased: boolean;
|
|
721
644
|
userAttendance: AttendanceRecord[];
|
|
722
645
|
userGroups: UserGroup[];
|
|
@@ -730,6 +653,7 @@ export const getBillableQuantityMapForGroupsRange = ({
|
|
|
730
653
|
now,
|
|
731
654
|
prepaidMonthCount,
|
|
732
655
|
selectedMonth,
|
|
656
|
+
workspaceTimezone,
|
|
733
657
|
useAttendanceBased,
|
|
734
658
|
userAttendance,
|
|
735
659
|
userGroups,
|
|
@@ -737,11 +661,9 @@ export const getBillableQuantityMapForGroupsRange = ({
|
|
|
737
661
|
])
|
|
738
662
|
);
|
|
739
663
|
|
|
740
|
-
/** Days before valid_until to consider "expiring soon" */
|
|
741
664
|
const EXPIRING_SOON_DAYS = 14;
|
|
742
665
|
|
|
743
666
|
export type GroupPaymentStatus = 'active' | 'expiringSoon' | 'expired';
|
|
744
|
-
|
|
745
667
|
export function getGroupPaymentStatus(
|
|
746
668
|
group: WorkspaceUserGroup | null,
|
|
747
669
|
latestInvoice:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { normalizeLegacyPanelSize } from './resizable';
|
|
3
|
+
|
|
4
|
+
describe('normalizeLegacyPanelSize', () => {
|
|
5
|
+
it('preserves the shared wrapper numeric percentage contract', () => {
|
|
6
|
+
expect(normalizeLegacyPanelSize(38)).toBe('38%');
|
|
7
|
+
expect(normalizeLegacyPanelSize(0)).toBe('0%');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('leaves explicitly unitized sizes unchanged', () => {
|
|
11
|
+
expect(normalizeLegacyPanelSize('20rem')).toBe('20rem');
|
|
12
|
+
expect(normalizeLegacyPanelSize('38%')).toBe('38%');
|
|
13
|
+
expect(normalizeLegacyPanelSize(undefined)).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -16,6 +16,13 @@ interface ResizablePanelGroupProps
|
|
|
16
16
|
onLayout?: (sizes: number[]) => void;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export function normalizeLegacyPanelSize(size: number | string | undefined) {
|
|
20
|
+
// react-resizable-panels v3 and the shadcn wrapper treated numeric values as
|
|
21
|
+
// percentages. v4 treats them as pixels, so preserve the shared wrapper's
|
|
22
|
+
// established contract while still allowing explicit CSS units.
|
|
23
|
+
return typeof size === 'number' ? `${size}%` : size;
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
function ResizablePanelGroup({
|
|
20
27
|
className,
|
|
21
28
|
direction,
|
|
@@ -26,11 +33,11 @@ function ResizablePanelGroup({
|
|
|
26
33
|
<Group
|
|
27
34
|
data-slot="resizable-panel-group"
|
|
28
35
|
orientation={direction}
|
|
29
|
-
|
|
36
|
+
onLayoutChanged={
|
|
30
37
|
onLayout
|
|
31
38
|
? (layout) => {
|
|
32
|
-
//
|
|
33
|
-
//
|
|
39
|
+
// Layout values are flex percentages (0-100). Preserve the
|
|
40
|
+
// wrapper's array callback for existing consumers.
|
|
34
41
|
const sizes = Object.values(layout);
|
|
35
42
|
onLayout(sizes);
|
|
36
43
|
}
|
|
@@ -45,8 +52,23 @@ function ResizablePanelGroup({
|
|
|
45
52
|
);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
function ResizablePanel({
|
|
49
|
-
|
|
55
|
+
function ResizablePanel({
|
|
56
|
+
collapsedSize,
|
|
57
|
+
defaultSize,
|
|
58
|
+
maxSize,
|
|
59
|
+
minSize,
|
|
60
|
+
...props
|
|
61
|
+
}: React.ComponentProps<typeof Panel>) {
|
|
62
|
+
return (
|
|
63
|
+
<Panel
|
|
64
|
+
collapsedSize={normalizeLegacyPanelSize(collapsedSize)}
|
|
65
|
+
data-slot="resizable-panel"
|
|
66
|
+
defaultSize={normalizeLegacyPanelSize(defaultSize)}
|
|
67
|
+
maxSize={normalizeLegacyPanelSize(maxSize)}
|
|
68
|
+
minSize={normalizeLegacyPanelSize(minSize)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
50
72
|
}
|
|
51
73
|
|
|
52
74
|
function ResizableHandle({
|
|
@@ -60,7 +82,7 @@ function ResizableHandle({
|
|
|
60
82
|
<Separator
|
|
61
83
|
data-slot="resizable-handle"
|
|
62
84
|
className={cn(
|
|
63
|
-
'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-
|
|
85
|
+
'relative flex w-px cursor-col-resize items-center justify-center bg-border transition-colors after:absolute after:inset-y-0 after:left-1/2 after:w-3 after:-translate-x-1/2 hover:bg-primary/60 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:cursor-row-resize data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-3 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90',
|
|
64
86
|
className
|
|
65
87
|
)}
|
|
66
88
|
{...props}
|
|
@@ -182,6 +182,7 @@ interface BoardColumnProps {
|
|
|
182
182
|
specialPinned?: boolean;
|
|
183
183
|
specialStickyOffset?: string;
|
|
184
184
|
onSpecialPinnedChange?: (pinned: boolean) => void;
|
|
185
|
+
onHoverTaskList?: (listId: string) => void;
|
|
185
186
|
readOnly?: boolean;
|
|
186
187
|
}
|
|
187
188
|
|
|
@@ -214,6 +215,7 @@ export function BoardColumn({
|
|
|
214
215
|
specialPinned = false,
|
|
215
216
|
specialStickyOffset,
|
|
216
217
|
onSpecialPinnedChange,
|
|
218
|
+
onHoverTaskList,
|
|
217
219
|
readOnly = false,
|
|
218
220
|
}: BoardColumnProps) {
|
|
219
221
|
const t = useTranslations('common');
|
|
@@ -530,6 +532,7 @@ export function BoardColumn({
|
|
|
530
532
|
data-kanban-pinned-special={specialStickyOffset ? 'true' : undefined}
|
|
531
533
|
data-kanban-column-id={column.id}
|
|
532
534
|
data-kanban-real-column={isExternalStaging ? undefined : 'true'}
|
|
535
|
+
onPointerEnter={() => onHoverTaskList?.(column.id)}
|
|
533
536
|
className={cn(
|
|
534
537
|
'group flex h-full w-14 shrink-0 snap-start flex-col items-center rounded-xl border border-dashed transition-all duration-200',
|
|
535
538
|
'touch-none select-none overflow-hidden hover:shadow-md',
|
|
@@ -583,6 +586,7 @@ export function BoardColumn({
|
|
|
583
586
|
data-kanban-pinned-special={specialStickyOffset ? 'true' : undefined}
|
|
584
587
|
data-kanban-column-id={column.id}
|
|
585
588
|
data-kanban-real-column={isExternalStaging ? undefined : 'true'}
|
|
589
|
+
onPointerEnter={() => onHoverTaskList?.(column.id)}
|
|
586
590
|
className={cn(
|
|
587
591
|
'group flex h-full w-[var(--kanban-column-width)] shrink-0 snap-start flex-col rounded-xl transition-all duration-200 last:snap-end',
|
|
588
592
|
'touch-none select-none',
|
package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx
CHANGED
|
@@ -187,4 +187,66 @@ describe('bulk move mutations', () => {
|
|
|
187
187
|
intensity: 1.2,
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
|
+
|
|
191
|
+
it('bulk-moves selected external tasks to done through terminal personal placement', async () => {
|
|
192
|
+
const doneList = {
|
|
193
|
+
...targetList,
|
|
194
|
+
id: 'done-list',
|
|
195
|
+
name: 'Done',
|
|
196
|
+
status: 'done',
|
|
197
|
+
} as unknown as TaskList;
|
|
198
|
+
|
|
199
|
+
queryClient.setQueryData(['tasks', 'board-1'], [localTask, externalTask]);
|
|
200
|
+
mockUpsertCurrentUserTaskPersonalPlacement.mockResolvedValueOnce({
|
|
201
|
+
task: {
|
|
202
|
+
...externalTask,
|
|
203
|
+
list_id: 'done-list',
|
|
204
|
+
personal_list_id: 'done-list',
|
|
205
|
+
source_list_id: 'source-done-list',
|
|
206
|
+
source_list_status: 'done',
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const { result } = renderHook(
|
|
211
|
+
() =>
|
|
212
|
+
useBulkOperations({
|
|
213
|
+
queryClient,
|
|
214
|
+
wsId: 'personal-ws',
|
|
215
|
+
boardId: 'board-1',
|
|
216
|
+
selectedTasks: new Set(['local-task', 'external-task']),
|
|
217
|
+
columns: [sourceList, doneList],
|
|
218
|
+
workspaceLabels: [],
|
|
219
|
+
workspaceProjects: [],
|
|
220
|
+
workspaceMembers: [],
|
|
221
|
+
setBulkWorking: vi.fn(),
|
|
222
|
+
clearSelection: vi.fn(),
|
|
223
|
+
setBulkDeleteOpen: vi.fn(),
|
|
224
|
+
}),
|
|
225
|
+
{ wrapper }
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await act(async () => {
|
|
229
|
+
await result.current.bulkMoveToList('done-list', 'Done');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(mockBulkWorkspaceTasks).toHaveBeenCalledWith(
|
|
233
|
+
'personal-ws',
|
|
234
|
+
{
|
|
235
|
+
taskIds: ['local-task'],
|
|
236
|
+
operation: {
|
|
237
|
+
type: 'move_to_list',
|
|
238
|
+
listId: 'done-list',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
expect.anything()
|
|
242
|
+
);
|
|
243
|
+
expect(mockUpsertCurrentUserTaskPersonalPlacement).toHaveBeenCalledWith(
|
|
244
|
+
'external-task',
|
|
245
|
+
expect.objectContaining({
|
|
246
|
+
personal_board_id: 'board-1',
|
|
247
|
+
personal_list_id: 'done-list',
|
|
248
|
+
terminal_status: 'done',
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
});
|
|
190
252
|
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { DragMoveEvent } from '@dnd-kit/core';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
getKanbanDragAutoScrollPointerX,
|
|
5
|
+
getKanbanEdgeAutoScrollAmount,
|
|
6
|
+
} from './auto-scroll';
|
|
7
|
+
|
|
8
|
+
const rect = {
|
|
9
|
+
left: 100,
|
|
10
|
+
right: 500,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function createDragMoveEvent({
|
|
14
|
+
deltaX = 0,
|
|
15
|
+
initialLeft = 200,
|
|
16
|
+
pointerStartX,
|
|
17
|
+
translatedLeft = initialLeft,
|
|
18
|
+
width = 80,
|
|
19
|
+
}: {
|
|
20
|
+
deltaX?: number;
|
|
21
|
+
initialLeft?: number;
|
|
22
|
+
pointerStartX?: number;
|
|
23
|
+
translatedLeft?: number | null;
|
|
24
|
+
width?: number;
|
|
25
|
+
}) {
|
|
26
|
+
return {
|
|
27
|
+
active: {
|
|
28
|
+
rect: {
|
|
29
|
+
current: {
|
|
30
|
+
initial: {
|
|
31
|
+
left: initialLeft,
|
|
32
|
+
width,
|
|
33
|
+
},
|
|
34
|
+
translated:
|
|
35
|
+
translatedLeft === null
|
|
36
|
+
? null
|
|
37
|
+
: {
|
|
38
|
+
left: translatedLeft,
|
|
39
|
+
width,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
activatorEvent:
|
|
45
|
+
pointerStartX === undefined
|
|
46
|
+
? ({ type: 'keydown' } as Event)
|
|
47
|
+
: ({ clientX: pointerStartX } as unknown as Event),
|
|
48
|
+
delta: {
|
|
49
|
+
x: deltaX,
|
|
50
|
+
y: 0,
|
|
51
|
+
},
|
|
52
|
+
} as unknown as DragMoveEvent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('getKanbanEdgeAutoScrollAmount', () => {
|
|
56
|
+
it('scrolls left when the drag center is near the left edge', () => {
|
|
57
|
+
expect(
|
|
58
|
+
getKanbanEdgeAutoScrollAmount(125, rect, {
|
|
59
|
+
threshold: 100,
|
|
60
|
+
speed: 10,
|
|
61
|
+
maxSpeed: 30,
|
|
62
|
+
})
|
|
63
|
+
).toBeLessThan(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('scrolls right when the drag center is near the right edge', () => {
|
|
67
|
+
expect(
|
|
68
|
+
getKanbanEdgeAutoScrollAmount(475, rect, {
|
|
69
|
+
threshold: 100,
|
|
70
|
+
speed: 10,
|
|
71
|
+
maxSpeed: 30,
|
|
72
|
+
})
|
|
73
|
+
).toBeGreaterThan(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('does nothing away from horizontal edges or without a drag center', () => {
|
|
77
|
+
expect(getKanbanEdgeAutoScrollAmount(300, rect)).toBe(0);
|
|
78
|
+
expect(getKanbanEdgeAutoScrollAmount(null, rect)).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('uses pointer position for edge auto-scroll instead of card center', () => {
|
|
82
|
+
const pointerX = getKanbanDragAutoScrollPointerX(
|
|
83
|
+
createDragMoveEvent({
|
|
84
|
+
deltaX: 215,
|
|
85
|
+
pointerStartX: 260,
|
|
86
|
+
translatedLeft: 190,
|
|
87
|
+
width: 80,
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(pointerX).toBe(475);
|
|
92
|
+
expect(
|
|
93
|
+
getKanbanEdgeAutoScrollAmount(pointerX, rect, {
|
|
94
|
+
threshold: 100,
|
|
95
|
+
speed: 10,
|
|
96
|
+
maxSpeed: 30,
|
|
97
|
+
})
|
|
98
|
+
).toBeGreaterThan(0);
|
|
99
|
+
expect(
|
|
100
|
+
getKanbanEdgeAutoScrollAmount(230, rect, {
|
|
101
|
+
threshold: 100,
|
|
102
|
+
speed: 10,
|
|
103
|
+
maxSpeed: 30,
|
|
104
|
+
})
|
|
105
|
+
).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('falls back to the active card center without pointer coordinates', () => {
|
|
109
|
+
expect(
|
|
110
|
+
getKanbanDragAutoScrollPointerX(
|
|
111
|
+
createDragMoveEvent({
|
|
112
|
+
translatedLeft: 370,
|
|
113
|
+
width: 100,
|
|
114
|
+
})
|
|
115
|
+
)
|
|
116
|
+
).toBe(420);
|
|
117
|
+
});
|
|
118
|
+
});
|