@tuturuuu/ui 0.30.1 → 0.30.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/biome.json +1 -1
- package/package.json +42 -42
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +49 -13
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -12
- package/src/components/ui/finance/invoices/utils.test.ts +6 -5
- package/src/components/ui/finance/invoices/utils.ts +3 -1
- package/src/components/ui/finance/transactions/columns.test.tsx +25 -1
- package/src/components/ui/finance/transactions/columns.tsx +45 -16
- package/src/components/ui/text-editor/task-item-checkbox-view.test.tsx +85 -0
- package/src/components/ui/text-editor/task-item-checkbox-view.tsx +28 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.30.2](https://github.com/tutur3u/platform/compare/ui-v0.30.1...ui-v0.30.2) (2026-08-31)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **finance:** allow advance subscription invoices ([d307068](https://github.com/tutur3u/platform/commit/d3070683a7c74b9f18f65dabdf482132fa02cf13))
|
|
9
|
+
* **tasks:** restore description checklist controls ([7976f78](https://github.com/tutur3u/platform/commit/7976f78292237e8047509e4243dafa14acea1b54))
|
|
10
|
+
|
|
3
11
|
## [0.30.1](https://github.com/tutur3u/platform/compare/ui-v0.30.0...ui-v0.30.1) (2026-08-27)
|
|
4
12
|
|
|
5
13
|
|
package/biome.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -57,36 +57,36 @@
|
|
|
57
57
|
"@streamdown/mermaid": "^1.0.2",
|
|
58
58
|
"@tanstack/react-hotkeys": "^0.10.0",
|
|
59
59
|
"@tanstack/react-pacer": "^0.23.0",
|
|
60
|
-
"@tanstack/react-query": "^5.
|
|
61
|
-
"@tanstack/react-table": "^9.
|
|
60
|
+
"@tanstack/react-query": "^5.102.8",
|
|
61
|
+
"@tanstack/react-table": "^9.2.3",
|
|
62
62
|
"@tanstack/react-virtual": "^3.14.10",
|
|
63
|
-
"@tiptap/core": "3.30.
|
|
64
|
-
"@tiptap/extension-collaboration": "3.30.
|
|
65
|
-
"@tiptap/extension-collaboration-caret": "3.30.
|
|
66
|
-
"@tiptap/extension-color": "3.30.
|
|
67
|
-
"@tiptap/extension-details": "3.30.
|
|
68
|
-
"@tiptap/extension-drag-handle": "3.30.
|
|
69
|
-
"@tiptap/extension-drag-handle-react": "3.30.
|
|
70
|
-
"@tiptap/extension-highlight": "3.30.
|
|
71
|
-
"@tiptap/extension-horizontal-rule": "3.30.
|
|
72
|
-
"@tiptap/extension-image": "3.30.
|
|
73
|
-
"@tiptap/extension-link": "3.30.
|
|
74
|
-
"@tiptap/extension-list": "3.30.
|
|
75
|
-
"@tiptap/extension-node-range": "3.30.
|
|
76
|
-
"@tiptap/extension-placeholder": "3.30.
|
|
77
|
-
"@tiptap/extension-strike": "3.30.
|
|
78
|
-
"@tiptap/extension-subscript": "3.30.
|
|
79
|
-
"@tiptap/extension-superscript": "3.30.
|
|
80
|
-
"@tiptap/extension-table": "3.30.
|
|
81
|
-
"@tiptap/extension-table-cell": "3.30.
|
|
82
|
-
"@tiptap/extension-table-header": "3.30.
|
|
83
|
-
"@tiptap/extension-table-row": "3.30.
|
|
84
|
-
"@tiptap/extension-text-align": "3.30.
|
|
85
|
-
"@tiptap/extension-text-style": "3.30.
|
|
86
|
-
"@tiptap/extension-youtube": "3.30.
|
|
87
|
-
"@tiptap/pm": "3.30.
|
|
88
|
-
"@tiptap/react": "3.30.
|
|
89
|
-
"@tiptap/starter-kit": "3.30.
|
|
63
|
+
"@tiptap/core": "3.30.5",
|
|
64
|
+
"@tiptap/extension-collaboration": "3.30.5",
|
|
65
|
+
"@tiptap/extension-collaboration-caret": "3.30.5",
|
|
66
|
+
"@tiptap/extension-color": "3.30.5",
|
|
67
|
+
"@tiptap/extension-details": "3.30.5",
|
|
68
|
+
"@tiptap/extension-drag-handle": "3.30.5",
|
|
69
|
+
"@tiptap/extension-drag-handle-react": "3.30.5",
|
|
70
|
+
"@tiptap/extension-highlight": "3.30.5",
|
|
71
|
+
"@tiptap/extension-horizontal-rule": "3.30.5",
|
|
72
|
+
"@tiptap/extension-image": "3.30.5",
|
|
73
|
+
"@tiptap/extension-link": "3.30.5",
|
|
74
|
+
"@tiptap/extension-list": "3.30.5",
|
|
75
|
+
"@tiptap/extension-node-range": "3.30.5",
|
|
76
|
+
"@tiptap/extension-placeholder": "3.30.5",
|
|
77
|
+
"@tiptap/extension-strike": "3.30.5",
|
|
78
|
+
"@tiptap/extension-subscript": "3.30.5",
|
|
79
|
+
"@tiptap/extension-superscript": "3.30.5",
|
|
80
|
+
"@tiptap/extension-table": "3.30.5",
|
|
81
|
+
"@tiptap/extension-table-cell": "3.30.5",
|
|
82
|
+
"@tiptap/extension-table-header": "3.30.5",
|
|
83
|
+
"@tiptap/extension-table-row": "3.30.5",
|
|
84
|
+
"@tiptap/extension-text-align": "3.30.5",
|
|
85
|
+
"@tiptap/extension-text-style": "3.30.5",
|
|
86
|
+
"@tiptap/extension-youtube": "3.30.5",
|
|
87
|
+
"@tiptap/pm": "3.30.5",
|
|
88
|
+
"@tiptap/react": "3.30.5",
|
|
89
|
+
"@tiptap/starter-kit": "3.30.5",
|
|
90
90
|
"@tuturuuu/ai": "0.8.3",
|
|
91
91
|
"@tuturuuu/apis": "0.11.3",
|
|
92
92
|
"@tuturuuu/hooks": "0.1.1",
|
|
@@ -108,18 +108,18 @@
|
|
|
108
108
|
"framer-motion": "^13.1.1",
|
|
109
109
|
"gsap": "^3.15.0",
|
|
110
110
|
"html-entities": "^2.6.0",
|
|
111
|
-
"html2canvas-pro": "^2.
|
|
111
|
+
"html2canvas-pro": "^2.4.0",
|
|
112
112
|
"input-otp": "^1.5.0",
|
|
113
113
|
"katex": "^0.18.4",
|
|
114
114
|
"lodash": "4.18.1",
|
|
115
115
|
"moment": "^2.30.1",
|
|
116
116
|
"motion": "^13.1.1",
|
|
117
|
-
"next": "^16.3.
|
|
118
|
-
"next-intl": "^4.
|
|
117
|
+
"next": "^16.3.3",
|
|
118
|
+
"next-intl": "^4.14.1",
|
|
119
119
|
"next-themes": "^0.4.6",
|
|
120
|
-
"nuqs": "^2.10.
|
|
120
|
+
"nuqs": "^2.10.1",
|
|
121
121
|
"prosemirror-state": "^1.4.4",
|
|
122
|
-
"prosemirror-view": "1.42.
|
|
122
|
+
"prosemirror-view": "1.42.3",
|
|
123
123
|
"qrcode.react": "^4.2.0",
|
|
124
124
|
"react": "^19.2.8",
|
|
125
125
|
"react-colorful": "^5.8.0",
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
"recharts": "3.10.1",
|
|
137
137
|
"slot-text": "^0.3.4",
|
|
138
138
|
"sonner": "^2.0.8",
|
|
139
|
-
"streamdown": "^2.
|
|
139
|
+
"streamdown": "^2.6.0",
|
|
140
140
|
"tailwind-scrollbar": "^4.0.2",
|
|
141
141
|
"tailwind-scrollbar-hide": "^4.0.0",
|
|
142
142
|
"tailwindcss-animate": "^1.0.7",
|
|
@@ -150,21 +150,21 @@
|
|
|
150
150
|
"devDependencies": {
|
|
151
151
|
"@tailwindcss/postcss": "^4.3.3",
|
|
152
152
|
"@tailwindcss/typography": "^0.5.20",
|
|
153
|
-
"@tanstack/react-query": "^5.102.
|
|
154
|
-
"@tanstack/react-table": "^9.
|
|
153
|
+
"@tanstack/react-query": "^5.102.8",
|
|
154
|
+
"@tanstack/react-table": "^9.2.3",
|
|
155
155
|
"@testing-library/jest-dom": "^7.0.1",
|
|
156
|
-
"@testing-library/react": "^16.3.
|
|
156
|
+
"@testing-library/react": "^16.3.3",
|
|
157
157
|
"@tuturuuu/types": "0.28.0",
|
|
158
158
|
"@tuturuuu/typescript-config": "0.1.2",
|
|
159
159
|
"@types/html2canvas": "^1.0.0",
|
|
160
160
|
"@types/lodash": "^4.17.25",
|
|
161
|
-
"@types/node": "^26.
|
|
161
|
+
"@types/node": "^26.4.0",
|
|
162
162
|
"@types/react": "^19.2.18",
|
|
163
|
-
"@types/react-dom": "^19.2.
|
|
163
|
+
"@types/react-dom": "^19.2.5",
|
|
164
164
|
"@types/react-resizable": "^4.0.0",
|
|
165
165
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
166
166
|
"@types/uuid": "^11.0.0",
|
|
167
|
-
"@vitejs/plugin-react": "^6.1.
|
|
167
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
168
168
|
"autoprefixer": "^10.5.4",
|
|
169
169
|
"jsdom": "^30.0.1",
|
|
170
170
|
"postcss": "^8.5.26",
|
|
@@ -30,15 +30,28 @@ const testState = vi.hoisted(() => {
|
|
|
30
30
|
|
|
31
31
|
return {
|
|
32
32
|
InvoicePaymentSettings: vi.fn(),
|
|
33
|
+
SubscriptionAttendanceSummary: vi.fn(),
|
|
33
34
|
categories: [] as Array<{ id: string; name: string }>,
|
|
34
35
|
groupIds: ['group-1'],
|
|
35
36
|
month: '2026-07',
|
|
37
|
+
setMonth: vi.fn(),
|
|
36
38
|
productSelectionInjected: false,
|
|
37
39
|
products: [product],
|
|
38
40
|
selectedProducts: [{ inventory, product, quantity: 1 }],
|
|
39
41
|
useCategories: vi.fn(),
|
|
40
42
|
useWallets: vi.fn(),
|
|
41
43
|
userId: 'user-1',
|
|
44
|
+
userGroups: [
|
|
45
|
+
{
|
|
46
|
+
workspace_user_groups: {
|
|
47
|
+
ending_date: null as string | null,
|
|
48
|
+
id: 'group-1',
|
|
49
|
+
name: 'Group 1',
|
|
50
|
+
sessions: ['2026-07-05'],
|
|
51
|
+
starting_date: '2026-07-01',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
42
55
|
wallets: [] as Array<{
|
|
43
56
|
currency: string;
|
|
44
57
|
id: string;
|
|
@@ -67,7 +80,7 @@ vi.mock('nuqs', () => ({
|
|
|
67
80
|
useQueryState: (key: string, options?: { defaultValue?: unknown }) => {
|
|
68
81
|
if (key === 'user_id') return [testState.userId, vi.fn()];
|
|
69
82
|
if (key === 'group_ids') return [testState.groupIds, vi.fn()];
|
|
70
|
-
if (key === 'month') return [testState.month,
|
|
83
|
+
if (key === 'month') return [testState.month, testState.setMonth];
|
|
71
84
|
return [options?.defaultValue ?? '', vi.fn()];
|
|
72
85
|
},
|
|
73
86
|
}));
|
|
@@ -113,7 +126,10 @@ vi.mock('./components/invoice-products-permission-warning', () => ({
|
|
|
113
126
|
}));
|
|
114
127
|
|
|
115
128
|
vi.mock('./components/subscription-attendance-summary', () => ({
|
|
116
|
-
SubscriptionAttendanceSummary: () =>
|
|
129
|
+
SubscriptionAttendanceSummary: (props: unknown) => {
|
|
130
|
+
testState.SubscriptionAttendanceSummary(props);
|
|
131
|
+
return null;
|
|
132
|
+
},
|
|
117
133
|
}));
|
|
118
134
|
|
|
119
135
|
vi.mock('./components/subscription-group-selector', () => ({
|
|
@@ -178,17 +194,7 @@ vi.mock('./hooks', () => ({
|
|
|
178
194
|
isLoading: false,
|
|
179
195
|
}),
|
|
180
196
|
useUserGroups: () => ({
|
|
181
|
-
data:
|
|
182
|
-
{
|
|
183
|
-
workspace_user_groups: {
|
|
184
|
-
ending_date: null,
|
|
185
|
-
id: 'group-1',
|
|
186
|
-
name: 'Group 1',
|
|
187
|
-
sessions: ['2026-07-05'],
|
|
188
|
-
starting_date: '2026-07-01',
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
],
|
|
197
|
+
data: testState.userGroups,
|
|
192
198
|
isLoading: false,
|
|
193
199
|
}),
|
|
194
200
|
useUserLinkedPromotions: () => ({ data: [] }),
|
|
@@ -262,9 +268,14 @@ function renderSubscriptionInvoice(
|
|
|
262
268
|
describe('SubscriptionInvoice checkout defaults', () => {
|
|
263
269
|
beforeEach(() => {
|
|
264
270
|
testState.InvoicePaymentSettings.mockClear();
|
|
271
|
+
testState.SubscriptionAttendanceSummary.mockClear();
|
|
265
272
|
testState.useCategories.mockClear();
|
|
266
273
|
testState.useWallets.mockClear();
|
|
267
274
|
testState.productSelectionInjected = false;
|
|
275
|
+
testState.month = '2026-07';
|
|
276
|
+
testState.setMonth.mockClear();
|
|
277
|
+
testState.userGroups[0]!.workspace_user_groups.ending_date = null;
|
|
278
|
+
testState.userGroups[0]!.workspace_user_groups.sessions = ['2026-07-05'];
|
|
268
279
|
testState.products[0]!.finance_category_id = null;
|
|
269
280
|
testState.wallets = [];
|
|
270
281
|
testState.categories = [];
|
|
@@ -328,4 +339,29 @@ describe('SubscriptionInvoice checkout defaults', () => {
|
|
|
328
339
|
)
|
|
329
340
|
);
|
|
330
341
|
});
|
|
342
|
+
|
|
343
|
+
it('keeps an advance month selected after the group schedule end', async () => {
|
|
344
|
+
testState.month = '2026-09';
|
|
345
|
+
testState.userGroups[0]!.workspace_user_groups.ending_date = '2026-08-31';
|
|
346
|
+
testState.userGroups[0]!.workspace_user_groups.sessions = [
|
|
347
|
+
'2026-07-05',
|
|
348
|
+
'2026-08-05',
|
|
349
|
+
'2026-09-05',
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
renderSubscriptionInvoice();
|
|
353
|
+
|
|
354
|
+
await waitFor(() =>
|
|
355
|
+
expect(testState.SubscriptionAttendanceSummary).toHaveBeenCalledWith(
|
|
356
|
+
expect.objectContaining({
|
|
357
|
+
availableMonths: expect.arrayContaining([
|
|
358
|
+
expect.objectContaining({ value: '2026-09' }),
|
|
359
|
+
]),
|
|
360
|
+
selectedMonth: '2026-09',
|
|
361
|
+
})
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
expect(testState.setMonth).not.toHaveBeenCalledWith('2026-07');
|
|
365
|
+
expect(testState.setMonth).not.toHaveBeenCalledWith('2026-08');
|
|
366
|
+
});
|
|
331
367
|
});
|
|
@@ -728,30 +728,22 @@ export function SubscriptionInvoice({
|
|
|
728
728
|
}
|
|
729
729
|
lastMonthSyncRef.current = null;
|
|
730
730
|
|
|
731
|
-
const { earliestStart
|
|
732
|
-
userGroups,
|
|
733
|
-
selectedGroupIds
|
|
734
|
-
);
|
|
731
|
+
const { earliestStart } = getGroupsDateRange(userGroups, selectedGroupIds);
|
|
735
732
|
|
|
736
|
-
if (!earliestStart
|
|
733
|
+
if (!earliestStart) return;
|
|
737
734
|
|
|
738
735
|
const currentMonth = getMonthStartDate(selectedMonth);
|
|
739
736
|
if (Number.isNaN(currentMonth.getTime())) return;
|
|
740
737
|
|
|
741
738
|
const earliestMonthStart = getMonthStartDate(earliestStart);
|
|
742
|
-
const latestMonthStart = getMonthStartDate(latestEnd);
|
|
743
739
|
|
|
744
|
-
if (currentMonth < earliestMonthStart
|
|
740
|
+
if (currentMonth < earliestMonthStart) {
|
|
745
741
|
// Only correct once per groups-selection to avoid update loops
|
|
746
742
|
if (lastValidatedKeyRef.current === monthValidationKey) return;
|
|
747
743
|
lastValidatedKeyRef.current = monthValidationKey;
|
|
748
744
|
|
|
749
745
|
const now = getCurrentBillingDate(workspaceTimezone);
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if (now >= earliestStart && now <= latestEnd) defaultMonth = now;
|
|
753
|
-
else if (now > latestEnd) defaultMonth = latestEnd;
|
|
754
|
-
else defaultMonth = earliestStart;
|
|
746
|
+
const defaultMonth = now >= earliestStart ? now : earliestStart;
|
|
755
747
|
|
|
756
748
|
const nextMonth = formatMonthValue(defaultMonth);
|
|
757
749
|
if (nextMonth !== selectedMonth) {
|
|
@@ -233,7 +233,7 @@ describe('subscription invoice coverage', () => {
|
|
|
233
233
|
).toBe(true);
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
-
it('
|
|
236
|
+
it('keeps future billing months available after a group ending date', () => {
|
|
237
237
|
const groups = [
|
|
238
238
|
{
|
|
239
239
|
workspace_user_groups: {
|
|
@@ -247,10 +247,11 @@ describe('subscription invoice coverage', () => {
|
|
|
247
247
|
] satisfies UserGroup[];
|
|
248
248
|
|
|
249
249
|
expect(
|
|
250
|
-
getAvailableMonths(groups, [groupId], [], 'en-US', '2026-06',
|
|
251
|
-
(
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
getAvailableMonths(groups, [groupId], [], 'en-US', '2026-06', 3, {
|
|
251
|
+
now: new Date(2026, 5, 15),
|
|
252
|
+
workspaceTimezone: 'UTC',
|
|
253
|
+
}).map((month) => month.value)
|
|
254
|
+
).toEqual(['2026-06', '2026-07', '2026-08', '2026-09']);
|
|
254
255
|
});
|
|
255
256
|
|
|
256
257
|
it('skips paid months and uses scheduled sessions for future prepaid quantities', () => {
|
|
@@ -524,7 +524,9 @@ export const getAvailableMonths = (
|
|
|
524
524
|
? getMonthStartDate(selectedMonthFallback)
|
|
525
525
|
: currentMonthStart;
|
|
526
526
|
const resolvedRangeEnd =
|
|
527
|
-
futureMonthHorizon > 0 &&
|
|
527
|
+
futureMonthHorizon > 0 && futureHorizonEnd > resolvedLatestEnd
|
|
528
|
+
? futureHorizonEnd
|
|
529
|
+
: resolvedLatestEnd;
|
|
528
530
|
|
|
529
531
|
const months: AvailableMonthOption[] = [];
|
|
530
532
|
const currentDate = new Date(earliestStart);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import type { ColumnDef } from '@tuturuuu/ui/custom/tables/data-table';
|
|
3
|
-
import {
|
|
3
|
+
import { renderToString } from 'react-dom/server';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { transactionColumns } from './columns';
|
|
5
6
|
|
|
6
7
|
vi.mock('next-intl', () => ({
|
|
@@ -82,6 +83,10 @@ describe('transactionColumns', () => {
|
|
|
82
83
|
'finance-confidential-mode=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;Secure';
|
|
83
84
|
});
|
|
84
85
|
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
vi.useRealTimers();
|
|
88
|
+
});
|
|
89
|
+
|
|
85
90
|
it('masks the amount column when finance numbers are globally hidden', () => {
|
|
86
91
|
render(<TransactionAmountCell amount={123} />);
|
|
87
92
|
|
|
@@ -116,4 +121,23 @@ describe('transactionColumns', () => {
|
|
|
116
121
|
expect(screen.getByText('Cash')).toBeVisible();
|
|
117
122
|
expect(screen.getByText('Food & Beverage')).toBeVisible();
|
|
118
123
|
});
|
|
124
|
+
|
|
125
|
+
it('keeps server-rendered transaction dates stable as time advances', () => {
|
|
126
|
+
vi.useFakeTimers();
|
|
127
|
+
vi.setSystemTime('2026-08-31T00:59:00.000Z');
|
|
128
|
+
|
|
129
|
+
const transaction = {
|
|
130
|
+
taken_at: '2026-08-31T00:00:00.000Z',
|
|
131
|
+
};
|
|
132
|
+
const firstRender = renderToString(
|
|
133
|
+
<TransactionColumnCell accessorKey="taken_at" original={transaction} />
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
vi.setSystemTime('2026-08-31T02:01:00.000Z');
|
|
137
|
+
const laterRender = renderToString(
|
|
138
|
+
<TransactionColumnCell accessorKey="taken_at" original={transaction} />
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(laterRender).toBe(firstRender);
|
|
142
|
+
});
|
|
119
143
|
});
|
|
@@ -12,6 +12,7 @@ import { formatCurrency } from '@tuturuuu/utils/format';
|
|
|
12
12
|
import moment from 'moment';
|
|
13
13
|
import 'moment/locale/vi';
|
|
14
14
|
import { useLocale } from 'next-intl';
|
|
15
|
+
import { useSyncExternalStore } from 'react';
|
|
15
16
|
import { DataTableColumnHeader } from '../../custom/tables/data-table-column-header';
|
|
16
17
|
import {
|
|
17
18
|
FINANCE_HIDDEN_AMOUNT,
|
|
@@ -27,6 +28,44 @@ interface TransactionExtraData {
|
|
|
27
28
|
isPersonalWorkspace?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
const subscribeToHydration = () => () => undefined;
|
|
32
|
+
const getHydratedSnapshot = () => true;
|
|
33
|
+
const getServerSnapshot = () => false;
|
|
34
|
+
|
|
35
|
+
function TransactionDateCell({
|
|
36
|
+
includeTime = false,
|
|
37
|
+
locale,
|
|
38
|
+
value,
|
|
39
|
+
}: {
|
|
40
|
+
includeTime?: boolean;
|
|
41
|
+
locale: string;
|
|
42
|
+
value: unknown;
|
|
43
|
+
}) {
|
|
44
|
+
const isHydrated = useSyncExternalStore(
|
|
45
|
+
subscribeToHydration,
|
|
46
|
+
getHydratedSnapshot,
|
|
47
|
+
getServerSnapshot
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (!value) return <div className="min-w-32">-</div>;
|
|
51
|
+
|
|
52
|
+
const localDate = moment(value);
|
|
53
|
+
if (!localDate.isValid()) return <div className="min-w-32">-</div>;
|
|
54
|
+
|
|
55
|
+
const format = includeTime ? 'DD/MM/YYYY, HH:mm:ss' : 'DD/MM/YYYY';
|
|
56
|
+
const absoluteDate = isHydrated
|
|
57
|
+
? localDate.locale(locale).format(format)
|
|
58
|
+
: moment.utc(value).locale(locale).format(format);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="min-w-32">
|
|
62
|
+
{isHydrated && !includeTime
|
|
63
|
+
? `${localDate.locale(locale).fromNow()}, ${absoluteDate}`
|
|
64
|
+
: absoluteDate}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
30
69
|
function TransactionAmountCell({
|
|
31
70
|
amount,
|
|
32
71
|
currency,
|
|
@@ -265,15 +304,7 @@ export const transactionColumns = ({
|
|
|
265
304
|
/>
|
|
266
305
|
),
|
|
267
306
|
cell: ({ row }) => (
|
|
268
|
-
<
|
|
269
|
-
{row.getValue('taken_at')
|
|
270
|
-
? `${moment(row.getValue('taken_at')).locale(locale).fromNow()}, ${moment(
|
|
271
|
-
row.getValue('taken_at')
|
|
272
|
-
)
|
|
273
|
-
.locale(locale)
|
|
274
|
-
.format('DD/MM/YYYY')}`
|
|
275
|
-
: '-'}
|
|
276
|
-
</div>
|
|
307
|
+
<TransactionDateCell locale={locale} value={row.getValue('taken_at')} />
|
|
277
308
|
),
|
|
278
309
|
},
|
|
279
310
|
{
|
|
@@ -286,13 +317,11 @@ export const transactionColumns = ({
|
|
|
286
317
|
/>
|
|
287
318
|
),
|
|
288
319
|
cell: ({ row }) => (
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
: '-'}
|
|
295
|
-
</div>
|
|
320
|
+
<TransactionDateCell
|
|
321
|
+
includeTime
|
|
322
|
+
locale={locale}
|
|
323
|
+
value={row.getValue('created_at')}
|
|
324
|
+
/>
|
|
296
325
|
),
|
|
297
326
|
},
|
|
298
327
|
{
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import type { NodeViewProps } from '@tiptap/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { TaskItemCheckboxContent } from './task-item-checkbox-view';
|
|
5
|
+
|
|
6
|
+
vi.mock('@tuturuuu/supabase/next/client', () => ({
|
|
7
|
+
createClient: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('./task-item-checkbox', async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal<typeof import('./task-item-checkbox')>();
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
useMentionedTaskStatuses: () => ({ data: [] }),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function createNodeViewProps() {
|
|
20
|
+
const setNodeMarkup = vi.fn();
|
|
21
|
+
const command = vi.fn(
|
|
22
|
+
(
|
|
23
|
+
callback: (props: {
|
|
24
|
+
tr: { setNodeMarkup: typeof setNodeMarkup };
|
|
25
|
+
}) => boolean
|
|
26
|
+
) => callback({ tr: { setNodeMarkup } })
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const props = {
|
|
30
|
+
node: {
|
|
31
|
+
attrs: { checked: false },
|
|
32
|
+
descendants: vi.fn(),
|
|
33
|
+
},
|
|
34
|
+
getPos: () => 4,
|
|
35
|
+
editor: {
|
|
36
|
+
isEditable: true,
|
|
37
|
+
commands: { command },
|
|
38
|
+
},
|
|
39
|
+
} as unknown as NodeViewProps;
|
|
40
|
+
|
|
41
|
+
return { command, props, setNodeMarkup };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('TaskItemCheckboxContent', () => {
|
|
45
|
+
it('cycles pointer interactions before the editor can replace the node view', () => {
|
|
46
|
+
const { props, setNodeMarkup } = createNodeViewProps();
|
|
47
|
+
render(<TaskItemCheckboxContent {...props} />);
|
|
48
|
+
|
|
49
|
+
const checkbox = screen.getByRole('checkbox', {
|
|
50
|
+
name: 'Task item status',
|
|
51
|
+
});
|
|
52
|
+
expect(checkbox).toHaveAttribute('aria-checked', 'false');
|
|
53
|
+
|
|
54
|
+
fireEvent.pointerDown(checkbox, { button: 0, isPrimary: true });
|
|
55
|
+
|
|
56
|
+
expect(setNodeMarkup).toHaveBeenLastCalledWith(4, undefined, {
|
|
57
|
+
checked: 'indeterminate',
|
|
58
|
+
});
|
|
59
|
+
expect(checkbox).toHaveAttribute('aria-checked', 'mixed');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('cycles all three states from the keyboard', () => {
|
|
63
|
+
const { props, setNodeMarkup } = createNodeViewProps();
|
|
64
|
+
render(<TaskItemCheckboxContent {...props} />);
|
|
65
|
+
|
|
66
|
+
const checkbox = screen.getByRole('checkbox', {
|
|
67
|
+
name: 'Task item status',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
fireEvent.keyDown(checkbox, { key: ' ' });
|
|
71
|
+
fireEvent.keyDown(checkbox, { key: 'Enter' });
|
|
72
|
+
fireEvent.keyDown(checkbox, { key: ' ' });
|
|
73
|
+
|
|
74
|
+
expect(setNodeMarkup).toHaveBeenNthCalledWith(1, 4, undefined, {
|
|
75
|
+
checked: 'indeterminate',
|
|
76
|
+
});
|
|
77
|
+
expect(setNodeMarkup).toHaveBeenNthCalledWith(2, 4, undefined, {
|
|
78
|
+
checked: true,
|
|
79
|
+
});
|
|
80
|
+
expect(setNodeMarkup).toHaveBeenNthCalledWith(3, 4, undefined, {
|
|
81
|
+
checked: false,
|
|
82
|
+
});
|
|
83
|
+
expect(checkbox).toHaveAttribute('aria-checked', 'false');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -68,14 +68,24 @@ export function TaskItemCheckboxContent({
|
|
|
68
68
|
});
|
|
69
69
|
}, [editor, getPos, node.attrs, checkboxState]);
|
|
70
70
|
|
|
71
|
-
const
|
|
72
|
-
(event: React.
|
|
71
|
+
const handleCheckboxPointerDown = useCallback(
|
|
72
|
+
(event: React.PointerEvent<HTMLButtonElement>) => {
|
|
73
|
+
if (!event.isPrimary || event.button !== 0) return;
|
|
73
74
|
event.preventDefault();
|
|
74
75
|
event.stopPropagation();
|
|
75
|
-
if (!editor.isEditable) return;
|
|
76
76
|
handleCheckboxCycle();
|
|
77
77
|
},
|
|
78
|
-
[
|
|
78
|
+
[handleCheckboxCycle]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const handleCheckboxKeyDown = useCallback(
|
|
82
|
+
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
83
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
event.stopPropagation();
|
|
86
|
+
handleCheckboxCycle();
|
|
87
|
+
},
|
|
88
|
+
[handleCheckboxCycle]
|
|
79
89
|
);
|
|
80
90
|
|
|
81
91
|
return (
|
|
@@ -84,29 +94,29 @@ export function TaskItemCheckboxContent({
|
|
|
84
94
|
className="task-list-checkbox-label flex shrink-0 select-none pt-2.5"
|
|
85
95
|
contentEditable={false}
|
|
86
96
|
>
|
|
97
|
+
{/* biome-ignore lint/a11y/useSemanticElements: a button avoids native checkbox state changes while ProseMirror owns the tri-state document update */}
|
|
87
98
|
<button
|
|
88
99
|
type="button"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
: isIndeterminate
|
|
93
|
-
? 'Indeterminate'
|
|
94
|
-
: 'Unchecked'
|
|
95
|
-
}
|
|
100
|
+
role="checkbox"
|
|
101
|
+
aria-label="Task item status"
|
|
102
|
+
aria-checked={isIndeterminate ? 'mixed' : isChecked}
|
|
96
103
|
aria-disabled={!editor.isEditable}
|
|
97
|
-
|
|
104
|
+
onPointerDown={handleCheckboxPointerDown}
|
|
105
|
+
onKeyDown={handleCheckboxKeyDown}
|
|
98
106
|
className={cn(
|
|
99
|
-
'task-list-checkbox flex
|
|
100
|
-
'cursor-pointer rounded-
|
|
101
|
-
'transition-
|
|
107
|
+
'task-list-checkbox flex size-5 items-center justify-center',
|
|
108
|
+
'cursor-pointer rounded-md border-2 bg-background',
|
|
109
|
+
'transition-[color,background-color,border-color,box-shadow,transform] duration-150 active:scale-95',
|
|
110
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35 focus-visible:ring-offset-2',
|
|
111
|
+
!editor.isEditable && 'cursor-default opacity-70',
|
|
102
112
|
!completedTaskColor && [
|
|
103
113
|
'border-input',
|
|
104
114
|
'hover:scale-105 hover:border-dynamic-gray hover:bg-dynamic-gray/10',
|
|
105
|
-
'focus:border-dynamic-gray
|
|
115
|
+
'focus-visible:border-dynamic-gray',
|
|
106
116
|
isChecked &&
|
|
107
|
-
'border-dynamic-green bg-dynamic-green/20 hover:border-dynamic-green hover:bg-dynamic-green/10 focus:border-dynamic-green focus:ring-dynamic-green/30',
|
|
117
|
+
'border-dynamic-green bg-dynamic-green/20 hover:border-dynamic-green hover:bg-dynamic-green/10 focus-visible:border-dynamic-green focus-visible:ring-dynamic-green/30',
|
|
108
118
|
isIndeterminate &&
|
|
109
|
-
'border-dynamic-orange bg-dynamic-orange/20 hover:border-dynamic-orange hover:bg-dynamic-orange/10 focus:border-dynamic-orange focus:ring-dynamic-orange/30',
|
|
119
|
+
'border-dynamic-orange bg-dynamic-orange/20 hover:border-dynamic-orange hover:bg-dynamic-orange/10 focus-visible:border-dynamic-orange focus-visible:ring-dynamic-orange/30',
|
|
110
120
|
],
|
|
111
121
|
completedTaskColor === 'red' && [
|
|
112
122
|
'border-dynamic-red/50',
|