@tuturuuu/ui 0.32.0 → 0.34.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 +62 -0
- package/biome.json +1 -1
- package/package.json +53 -51
- package/src/components/ui/calendar-app/components/calendar-sync-attention-button.tsx +58 -24
- package/src/components/ui/calendar-app/components/calendar-sync-recovery.test.tsx +57 -0
- package/src/components/ui/calendar-app/components/calendar-sync-recovery.tsx +28 -14
- package/src/components/ui/calendar-app/components/refresh-calendar-sync-status.test.tsx +321 -0
- package/src/components/ui/calendar-app/components/refresh-calendar-sync-status.ts +182 -0
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +12 -14
- package/src/components/ui/chat/ai-message-parts.test.tsx +47 -0
- package/src/components/ui/chat/ai-message-parts.tsx +23 -16
- package/src/components/ui/chat/ai-message-render-utils.ts +19 -1
- package/src/components/ui/commercial-comparison-style.ts +104 -0
- package/src/components/ui/commercial-comparison-table.tsx +159 -0
- package/src/components/ui/commercial-comparison-toolbar.tsx +205 -0
- package/src/components/ui/commercial-comparison.test.tsx +86 -0
- package/src/components/ui/commercial-comparison.tsx +224 -0
- package/src/components/ui/comparison-value.test.tsx +32 -0
- package/src/components/ui/comparison-value.tsx +72 -0
- package/src/components/ui/custom/__tests__/report-preview-pagination-progress.test.ts +129 -0
- package/src/components/ui/custom/__tests__/report-preview-viewport.test.tsx +56 -0
- package/src/components/ui/custom/account-assurance-refresh.test.tsx +42 -0
- package/src/components/ui/custom/account-assurance-refresh.tsx +30 -0
- package/src/components/ui/custom/education/modules/resources/pdf-viewer.tsx +13 -2
- package/src/components/ui/custom/report-preview-pagination.ts +5 -3
- package/src/components/ui/custom/report-preview-viewport.tsx +40 -0
- package/src/components/ui/custom/report-preview.tsx +2 -3
- package/src/components/ui/finance/categories-tags-tabs.tsx +3 -0
- package/src/components/ui/finance/invoices/charts/subscription-payment-chart.tsx +137 -0
- package/src/components/ui/finance/invoices/charts/subscription-payment-details.tsx +111 -0
- package/src/components/ui/finance/invoices/columns.tsx +2 -0
- package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +6 -3
- package/src/components/ui/finance/invoices/components/invoice-data-state.test.tsx +27 -0
- package/src/components/ui/finance/invoices/components/invoice-data-state.tsx +34 -0
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +29 -6
- package/src/components/ui/finance/invoices/export-data.test.ts +121 -0
- package/src/components/ui/finance/invoices/export-data.ts +229 -0
- package/src/components/ui/finance/invoices/export-dialog-content.test.tsx +245 -0
- package/src/components/ui/finance/invoices/export-dialog-content.tsx +108 -292
- package/src/components/ui/finance/invoices/export-pagination.test.ts +131 -0
- package/src/components/ui/finance/invoices/export-pagination.ts +66 -0
- package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.test.tsx +89 -0
- package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.ts +117 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.test.tsx +255 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +6 -92
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.test.tsx +93 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +11 -15
- package/src/components/ui/finance/invoices/hooks.ts +5 -110
- package/src/components/ui/finance/invoices/internal-api.test.ts +122 -0
- package/src/components/ui/finance/invoices/internal-api.ts +19 -1
- package/src/components/ui/finance/invoices/invoice-analytics.tsx +24 -1
- package/src/components/ui/finance/invoices/invoice-discount.test.tsx +107 -0
- package/src/components/ui/finance/invoices/invoice-discount.ts +12 -0
- package/src/components/ui/finance/invoices/invoice-history-filters.tsx +121 -0
- package/src/components/ui/finance/invoices/invoice-history-pagination.tsx +81 -0
- package/src/components/ui/finance/invoices/invoice-history-row.tsx +159 -0
- package/src/components/ui/finance/invoices/invoice-history.test.tsx +170 -0
- package/src/components/ui/finance/invoices/invoice-history.tsx +300 -0
- package/src/components/ui/finance/invoices/invoice-page.tsx +18 -2
- package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +3 -7
- package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.test.tsx +16 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +9 -0
- package/src/components/ui/finance/invoices/invoices-table.tsx +3 -0
- package/src/components/ui/finance/invoices/promotion-form.tsx +5 -4
- package/src/components/ui/finance/invoices/query-invalidation.test.ts +28 -0
- package/src/components/ui/finance/invoices/query-invalidation.ts +3 -0
- package/src/components/ui/finance/invoices/row-actions.tsx +35 -4
- package/src/components/ui/finance/invoices/standard-invoice.tsx +27 -16
- package/src/components/ui/finance/invoices/subscription-coverage.test.ts +74 -0
- package/src/components/ui/finance/invoices/subscription-coverage.ts +109 -0
- package/src/components/ui/finance/invoices/subscription-invoice-props.ts +20 -0
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +71 -4
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +47 -51
- package/src/components/ui/finance/invoices/subscription-payment-analytics.test.tsx +168 -0
- package/src/components/ui/finance/invoices/subscription-payment-analytics.tsx +253 -0
- package/src/components/ui/finance/invoices/utils.ts +48 -106
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-body.tsx +73 -80
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-controls.tsx +3 -1
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-legend.test.tsx +30 -0
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-legend.tsx +31 -24
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-tooltip.tsx +9 -6
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-types.ts +2 -0
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-utils.test.ts +59 -0
- package/src/components/ui/finance/shared/charts/category-breakdown-chart-utils.ts +51 -37
- package/src/components/ui/finance/shared/charts/category-breakdown-chart.tsx +37 -7
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +5 -5
- package/src/components/ui/legacy/calendar/location-timeline.tsx +2 -14
- package/src/components/ui/legacy/calendar/working-location.test.ts +47 -0
- package/src/components/ui/legacy/calendar/working-location.ts +35 -0
- package/src/components/ui/public-workspace-prices.tsx +13 -0
- package/src/components/ui/pwa-status.test.tsx +13 -2
- package/src/components/ui/pwa-status.tsx +15 -3
- package/src/components/ui/text-editor/editor-readonly-transition.test.tsx +86 -0
- package/src/components/ui/text-editor/editor.tsx +11 -3
- package/src/components/ui/text-editor/task-item-checkbox-view.test.tsx +188 -65
- package/src/components/ui/text-editor/task-item-checkbox-view.tsx +178 -164
- package/src/components/ui/text-editor/task-item-status-picker.tsx +130 -0
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +20 -1
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +99 -180
- package/src/hooks/calendar-provider-sync.ts +33 -0
- package/src/hooks/notification-subscription.ts +74 -0
- package/src/hooks/use-calendar-sync.tsx +11 -14
- package/src/hooks/use-calendar.tsx +2 -2
- package/src/hooks/use-notifications.ts +5 -141
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import type { NodeViewProps } from '@tiptap/react';
|
|
4
4
|
import { NodeViewContent } from '@tiptap/react';
|
|
5
5
|
import { cn } from '@tuturuuu/utils/format';
|
|
6
|
-
import {
|
|
6
|
+
import { useTranslations } from 'next-intl';
|
|
7
|
+
import { useCallback, useMemo, useSyncExternalStore } from 'react';
|
|
7
8
|
import { DraggableNodeContainer } from './draggable-node-container';
|
|
8
9
|
import {
|
|
9
10
|
areAllMentionedTasksCompleted,
|
|
@@ -14,16 +15,31 @@ import {
|
|
|
14
15
|
type TriStateChecked,
|
|
15
16
|
useMentionedTaskStatuses,
|
|
16
17
|
} from './task-item-checkbox';
|
|
18
|
+
import { TaskItemStatusPicker } from './task-item-status-picker';
|
|
17
19
|
|
|
18
20
|
export function TaskItemCheckboxContent({
|
|
19
21
|
node,
|
|
20
22
|
getPos,
|
|
21
23
|
editor,
|
|
22
24
|
}: NodeViewProps) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
+
const t = useTranslations('common.checklist');
|
|
26
|
+
const subscribe = useCallback(
|
|
27
|
+
(notify: () => void) => {
|
|
28
|
+
// setEditable emits update without changing the document/node view.
|
|
29
|
+
editor.on('update', notify);
|
|
30
|
+
editor.on('transaction', notify);
|
|
31
|
+
return () => {
|
|
32
|
+
editor.off('update', notify);
|
|
33
|
+
editor.off('transaction', notify);
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
[editor]
|
|
37
|
+
);
|
|
38
|
+
const isEditable = useSyncExternalStore(
|
|
39
|
+
subscribe,
|
|
40
|
+
() => editor.isEditable,
|
|
41
|
+
() => false
|
|
25
42
|
);
|
|
26
|
-
|
|
27
43
|
const taskMentionIds = useMemo(() => {
|
|
28
44
|
return extractTaskMentionIds(node);
|
|
29
45
|
}, [node]);
|
|
@@ -40,187 +56,185 @@ export function TaskItemCheckboxContent({
|
|
|
40
56
|
|
|
41
57
|
const checkboxState = useMemo((): TriStateChecked => {
|
|
42
58
|
return resolveCheckboxState({
|
|
43
|
-
manualOverride,
|
|
59
|
+
manualOverride: null,
|
|
44
60
|
nodeChecked: node.attrs.checked,
|
|
45
61
|
allMentionedTasksCompleted,
|
|
46
62
|
});
|
|
47
|
-
}, [
|
|
63
|
+
}, [node.attrs.checked, allMentionedTasksCompleted]);
|
|
48
64
|
|
|
49
65
|
const isChecked = checkboxState === true;
|
|
50
66
|
const isIndeterminate = checkboxState === 'indeterminate';
|
|
51
67
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
const setCheckboxState = useCallback(
|
|
69
|
+
(
|
|
70
|
+
checked: TriStateChecked | ((current: TriStateChecked) => TriStateChecked)
|
|
71
|
+
) => {
|
|
72
|
+
if (!editor.isEditable) return;
|
|
73
|
+
const pos = getPos();
|
|
74
|
+
if (typeof pos !== 'number') return;
|
|
75
|
+
editor.commands.command(({ tr }) => {
|
|
76
|
+
const currentNode = tr.doc.nodeAt(pos);
|
|
77
|
+
if (currentNode?.type.name !== 'taskItem') return false;
|
|
78
|
+
// Node view props can lag behind a transaction (e.g. a picker click).
|
|
79
|
+
// Cycle from the document so rapid clicks never repeat a stale state.
|
|
80
|
+
const current = resolveCheckboxState({
|
|
81
|
+
manualOverride: null,
|
|
82
|
+
nodeChecked: currentNode.attrs.checked,
|
|
83
|
+
allMentionedTasksCompleted,
|
|
84
|
+
});
|
|
85
|
+
tr.setNodeMarkup(pos, undefined, {
|
|
86
|
+
...currentNode.attrs,
|
|
87
|
+
checked: typeof checked === 'function' ? checked(current) : checked,
|
|
88
|
+
});
|
|
89
|
+
return true;
|
|
66
90
|
});
|
|
67
|
-
return true;
|
|
68
|
-
});
|
|
69
|
-
}, [editor, getPos, node.attrs, checkboxState]);
|
|
70
|
-
|
|
71
|
-
const handleCheckboxPointerDown = useCallback(
|
|
72
|
-
(event: React.PointerEvent<HTMLButtonElement>) => {
|
|
73
|
-
if (!event.isPrimary || event.button !== 0) return;
|
|
74
|
-
event.preventDefault();
|
|
75
|
-
event.stopPropagation();
|
|
76
|
-
handleCheckboxCycle();
|
|
77
|
-
},
|
|
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
91
|
},
|
|
88
|
-
[
|
|
92
|
+
[editor, getPos, allMentionedTasksCompleted]
|
|
89
93
|
);
|
|
90
94
|
|
|
91
95
|
return (
|
|
92
|
-
<div className="flex items-start gap-2">
|
|
96
|
+
<div className="flex min-w-0 flex-1 items-start gap-2">
|
|
93
97
|
<div
|
|
94
98
|
className="task-list-checkbox-label flex shrink-0 select-none pt-2.5"
|
|
95
99
|
contentEditable={false}
|
|
96
100
|
>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
aria-label="Task item status"
|
|
102
|
-
aria-checked={isIndeterminate ? 'mixed' : isChecked}
|
|
103
|
-
aria-disabled={!editor.isEditable}
|
|
104
|
-
onPointerDown={handleCheckboxPointerDown}
|
|
105
|
-
onKeyDown={handleCheckboxKeyDown}
|
|
106
|
-
className={cn(
|
|
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',
|
|
112
|
-
!completedTaskColor && [
|
|
113
|
-
'border-input',
|
|
114
|
-
'hover:scale-105 hover:border-dynamic-gray hover:bg-dynamic-gray/10',
|
|
115
|
-
'focus-visible:border-dynamic-gray',
|
|
116
|
-
isChecked &&
|
|
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',
|
|
118
|
-
isIndeterminate &&
|
|
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',
|
|
120
|
-
],
|
|
121
|
-
completedTaskColor === 'red' && [
|
|
122
|
-
'border-dynamic-red/50',
|
|
123
|
-
'hover:scale-105 hover:border-dynamic-red hover:bg-dynamic-red/10',
|
|
124
|
-
'focus:border-dynamic-red focus:outline-none focus:ring-2 focus:ring-dynamic-red/30 focus:ring-offset-2',
|
|
125
|
-
(isChecked || isIndeterminate) &&
|
|
126
|
-
'border-dynamic-red bg-dynamic-red/20',
|
|
127
|
-
],
|
|
128
|
-
completedTaskColor === 'orange' && [
|
|
129
|
-
'border-dynamic-orange/50',
|
|
130
|
-
'hover:scale-105 hover:border-dynamic-orange hover:bg-dynamic-orange/10',
|
|
131
|
-
'focus:border-dynamic-orange focus:outline-none focus:ring-2 focus:ring-dynamic-orange/30 focus:ring-offset-2',
|
|
132
|
-
(isChecked || isIndeterminate) &&
|
|
133
|
-
'border-dynamic-orange bg-dynamic-orange/20',
|
|
134
|
-
],
|
|
135
|
-
completedTaskColor === 'yellow' && [
|
|
136
|
-
'border-dynamic-yellow/50',
|
|
137
|
-
'hover:scale-105 hover:border-dynamic-yellow hover:bg-dynamic-yellow/10',
|
|
138
|
-
'focus:border-dynamic-yellow focus:outline-none focus:ring-2 focus:ring-dynamic-yellow/30 focus:ring-offset-2',
|
|
139
|
-
(isChecked || isIndeterminate) &&
|
|
140
|
-
'border-dynamic-yellow bg-dynamic-yellow/20',
|
|
141
|
-
],
|
|
142
|
-
completedTaskColor === 'green' && [
|
|
143
|
-
'border-dynamic-green/50',
|
|
144
|
-
'hover:scale-105 hover:border-dynamic-green hover:bg-dynamic-green/10',
|
|
145
|
-
'focus:border-dynamic-green focus:outline-none focus:ring-2 focus:ring-dynamic-green/30 focus:ring-offset-2',
|
|
146
|
-
(isChecked || isIndeterminate) &&
|
|
147
|
-
'border-dynamic-green bg-dynamic-green/20',
|
|
148
|
-
],
|
|
149
|
-
completedTaskColor === 'cyan' && [
|
|
150
|
-
'border-dynamic-cyan/50',
|
|
151
|
-
'hover:scale-105 hover:border-dynamic-cyan hover:bg-dynamic-cyan/10',
|
|
152
|
-
'focus:border-dynamic-cyan focus:outline-none focus:ring-2 focus:ring-dynamic-cyan/30 focus:ring-offset-2',
|
|
153
|
-
(isChecked || isIndeterminate) &&
|
|
154
|
-
'border-dynamic-cyan bg-dynamic-cyan/20',
|
|
155
|
-
],
|
|
156
|
-
completedTaskColor === 'blue' && [
|
|
157
|
-
'border-dynamic-blue/50',
|
|
158
|
-
'hover:scale-105 hover:border-dynamic-blue hover:bg-dynamic-blue/10',
|
|
159
|
-
'focus:border-dynamic-blue focus:outline-none focus:ring-2 focus:ring-dynamic-blue/30 focus:ring-offset-2',
|
|
160
|
-
(isChecked || isIndeterminate) &&
|
|
161
|
-
'border-dynamic-blue bg-dynamic-blue/20',
|
|
162
|
-
],
|
|
163
|
-
completedTaskColor === 'purple' && [
|
|
164
|
-
'border-dynamic-purple/50',
|
|
165
|
-
'hover:scale-105 hover:border-dynamic-purple hover:bg-dynamic-purple/10',
|
|
166
|
-
'focus:border-dynamic-purple focus:outline-none focus:ring-2 focus:ring-dynamic-purple/30 focus:ring-offset-2',
|
|
167
|
-
(isChecked || isIndeterminate) &&
|
|
168
|
-
'border-dynamic-purple bg-dynamic-purple/20',
|
|
169
|
-
],
|
|
170
|
-
completedTaskColor === 'pink' && [
|
|
171
|
-
'border-dynamic-pink/50',
|
|
172
|
-
'hover:scale-105 hover:border-dynamic-pink hover:bg-dynamic-pink/10',
|
|
173
|
-
'focus:border-dynamic-pink focus:outline-none focus:ring-2 focus:ring-dynamic-pink/30 focus:ring-offset-2',
|
|
174
|
-
(isChecked || isIndeterminate) &&
|
|
175
|
-
'border-dynamic-pink bg-dynamic-pink/20',
|
|
176
|
-
]
|
|
177
|
-
)}
|
|
101
|
+
<TaskItemStatusPicker
|
|
102
|
+
state={checkboxState}
|
|
103
|
+
disabled={!isEditable}
|
|
104
|
+
onSelect={setCheckboxState}
|
|
178
105
|
>
|
|
179
|
-
{
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
106
|
+
{/* biome-ignore lint/a11y/useSemanticElements: a button avoids native checkbox state changes while ProseMirror owns the tri-state document update */}
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
role="checkbox"
|
|
110
|
+
aria-label={t('status')}
|
|
111
|
+
aria-keyshortcuts="ArrowDown"
|
|
112
|
+
aria-checked={isIndeterminate ? 'mixed' : isChecked}
|
|
113
|
+
aria-disabled={!isEditable}
|
|
114
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
115
|
+
onClick={(event) => {
|
|
116
|
+
event.stopPropagation();
|
|
117
|
+
setCheckboxState(getNextTriState);
|
|
118
|
+
}}
|
|
119
|
+
className={cn(
|
|
120
|
+
'task-list-checkbox flex size-5 items-center justify-center',
|
|
121
|
+
'cursor-pointer rounded-md border-2 bg-background',
|
|
122
|
+
'transition-[color,background-color,border-color,box-shadow,transform] duration-150 active:scale-95',
|
|
123
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35 focus-visible:ring-offset-2',
|
|
124
|
+
!isEditable && 'cursor-default opacity-70',
|
|
125
|
+
!completedTaskColor && [
|
|
126
|
+
'border-muted-foreground/50',
|
|
127
|
+
'hover:scale-105 hover:border-dynamic-gray hover:bg-dynamic-gray/10',
|
|
128
|
+
'focus-visible:border-dynamic-gray',
|
|
129
|
+
isChecked &&
|
|
130
|
+
'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',
|
|
131
|
+
isIndeterminate &&
|
|
132
|
+
'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',
|
|
133
|
+
],
|
|
134
|
+
completedTaskColor === 'red' && [
|
|
135
|
+
'border-dynamic-red/50',
|
|
136
|
+
'hover:scale-105 hover:border-dynamic-red hover:bg-dynamic-red/10',
|
|
137
|
+
'focus:border-dynamic-red focus:outline-none focus:ring-2 focus:ring-dynamic-red/30 focus:ring-offset-2',
|
|
138
|
+
(isChecked || isIndeterminate) &&
|
|
139
|
+
'border-dynamic-red bg-dynamic-red/20',
|
|
140
|
+
],
|
|
141
|
+
completedTaskColor === 'orange' && [
|
|
142
|
+
'border-dynamic-orange/50',
|
|
143
|
+
'hover:scale-105 hover:border-dynamic-orange hover:bg-dynamic-orange/10',
|
|
144
|
+
'focus:border-dynamic-orange focus:outline-none focus:ring-2 focus:ring-dynamic-orange/30 focus:ring-offset-2',
|
|
145
|
+
(isChecked || isIndeterminate) &&
|
|
146
|
+
'border-dynamic-orange bg-dynamic-orange/20',
|
|
147
|
+
],
|
|
148
|
+
completedTaskColor === 'yellow' && [
|
|
149
|
+
'border-dynamic-yellow/50',
|
|
150
|
+
'hover:scale-105 hover:border-dynamic-yellow hover:bg-dynamic-yellow/10',
|
|
151
|
+
'focus:border-dynamic-yellow focus:outline-none focus:ring-2 focus:ring-dynamic-yellow/30 focus:ring-offset-2',
|
|
152
|
+
(isChecked || isIndeterminate) &&
|
|
153
|
+
'border-dynamic-yellow bg-dynamic-yellow/20',
|
|
154
|
+
],
|
|
155
|
+
completedTaskColor === 'green' && [
|
|
156
|
+
'border-dynamic-green/50',
|
|
157
|
+
'hover:scale-105 hover:border-dynamic-green hover:bg-dynamic-green/10',
|
|
158
|
+
'focus:border-dynamic-green focus:outline-none focus:ring-2 focus:ring-dynamic-green/30 focus:ring-offset-2',
|
|
159
|
+
(isChecked || isIndeterminate) &&
|
|
160
|
+
'border-dynamic-green bg-dynamic-green/20',
|
|
161
|
+
],
|
|
162
|
+
completedTaskColor === 'cyan' && [
|
|
163
|
+
'border-dynamic-cyan/50',
|
|
164
|
+
'hover:scale-105 hover:border-dynamic-cyan hover:bg-dynamic-cyan/10',
|
|
165
|
+
'focus:border-dynamic-cyan focus:outline-none focus:ring-2 focus:ring-dynamic-cyan/30 focus:ring-offset-2',
|
|
166
|
+
(isChecked || isIndeterminate) &&
|
|
167
|
+
'border-dynamic-cyan bg-dynamic-cyan/20',
|
|
168
|
+
],
|
|
169
|
+
completedTaskColor === 'blue' && [
|
|
170
|
+
'border-dynamic-blue/50',
|
|
171
|
+
'hover:scale-105 hover:border-dynamic-blue hover:bg-dynamic-blue/10',
|
|
172
|
+
'focus:border-dynamic-blue focus:outline-none focus:ring-2 focus:ring-dynamic-blue/30 focus:ring-offset-2',
|
|
173
|
+
(isChecked || isIndeterminate) &&
|
|
174
|
+
'border-dynamic-blue bg-dynamic-blue/20',
|
|
175
|
+
],
|
|
176
|
+
completedTaskColor === 'purple' && [
|
|
177
|
+
'border-dynamic-purple/50',
|
|
178
|
+
'hover:scale-105 hover:border-dynamic-purple hover:bg-dynamic-purple/10',
|
|
179
|
+
'focus:border-dynamic-purple focus:outline-none focus:ring-2 focus:ring-dynamic-purple/30 focus:ring-offset-2',
|
|
180
|
+
(isChecked || isIndeterminate) &&
|
|
181
|
+
'border-dynamic-purple bg-dynamic-purple/20',
|
|
182
|
+
],
|
|
183
|
+
completedTaskColor === 'pink' && [
|
|
184
|
+
'border-dynamic-pink/50',
|
|
185
|
+
'hover:scale-105 hover:border-dynamic-pink hover:bg-dynamic-pink/10',
|
|
186
|
+
'focus:border-dynamic-pink focus:outline-none focus:ring-2 focus:ring-dynamic-pink/30 focus:ring-offset-2',
|
|
187
|
+
(isChecked || isIndeterminate) &&
|
|
188
|
+
'border-dynamic-pink bg-dynamic-pink/20',
|
|
189
|
+
]
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{isChecked && (
|
|
193
|
+
<svg
|
|
194
|
+
className={cn(
|
|
195
|
+
'h-3.5 w-3.5',
|
|
196
|
+
completedTaskColor ? 'text-foreground' : 'text-dynamic-green'
|
|
197
|
+
)}
|
|
198
|
+
viewBox="0 0 16 16"
|
|
199
|
+
fill="none"
|
|
200
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
201
|
+
aria-hidden="true"
|
|
202
|
+
>
|
|
203
|
+
<path
|
|
204
|
+
d="M4 8l2.5 2.5L12 5"
|
|
205
|
+
stroke="currentColor"
|
|
206
|
+
strokeWidth="2"
|
|
207
|
+
strokeLinecap="round"
|
|
208
|
+
strokeLinejoin="round"
|
|
209
|
+
/>
|
|
210
|
+
</svg>
|
|
211
|
+
)}
|
|
212
|
+
{isIndeterminate && (
|
|
213
|
+
<svg
|
|
214
|
+
className={cn(
|
|
215
|
+
'h-3.5 w-3.5',
|
|
216
|
+
completedTaskColor ? 'text-foreground' : 'text-dynamic-orange'
|
|
217
|
+
)}
|
|
218
|
+
viewBox="0 0 16 16"
|
|
219
|
+
fill="none"
|
|
220
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
221
|
+
aria-hidden="true"
|
|
222
|
+
>
|
|
223
|
+
<path
|
|
224
|
+
d="M4 8h8"
|
|
225
|
+
stroke="currentColor"
|
|
226
|
+
strokeWidth="2"
|
|
227
|
+
strokeLinecap="round"
|
|
228
|
+
/>
|
|
229
|
+
</svg>
|
|
230
|
+
)}
|
|
231
|
+
</button>
|
|
232
|
+
</TaskItemStatusPicker>
|
|
219
233
|
</div>
|
|
220
234
|
|
|
221
235
|
<NodeViewContent
|
|
222
236
|
className={cn(
|
|
223
|
-
'transition-opacity duration-150',
|
|
237
|
+
'min-w-0 flex-1 cursor-text caret-foreground transition-opacity duration-150',
|
|
224
238
|
isChecked && 'task-content-faded'
|
|
225
239
|
)}
|
|
226
240
|
/>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Check, Minus, Square } from '@tuturuuu/icons';
|
|
4
|
+
import { useTranslations } from 'next-intl';
|
|
5
|
+
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
|
6
|
+
import { Popover, PopoverAnchor, PopoverContent } from '../popover';
|
|
7
|
+
import type { TriStateChecked } from './task-item-checkbox';
|
|
8
|
+
|
|
9
|
+
export function TaskItemStatusPicker({
|
|
10
|
+
children,
|
|
11
|
+
state,
|
|
12
|
+
disabled,
|
|
13
|
+
onSelect,
|
|
14
|
+
}: {
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
state: TriStateChecked;
|
|
17
|
+
disabled: boolean;
|
|
18
|
+
onSelect: (state: TriStateChecked) => void;
|
|
19
|
+
}) {
|
|
20
|
+
const t = useTranslations('common.checklist');
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
22
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
23
|
+
const keyboardOpen = useRef(false);
|
|
24
|
+
const anchor = useRef<HTMLDivElement>(null);
|
|
25
|
+
const cancelTimer = () => {
|
|
26
|
+
if (timer.current) clearTimeout(timer.current);
|
|
27
|
+
timer.current = null;
|
|
28
|
+
};
|
|
29
|
+
useEffect(
|
|
30
|
+
() => () => {
|
|
31
|
+
if (timer.current) clearTimeout(timer.current);
|
|
32
|
+
},
|
|
33
|
+
[]
|
|
34
|
+
);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!disabled) return;
|
|
37
|
+
if (timer.current) clearTimeout(timer.current);
|
|
38
|
+
timer.current = null;
|
|
39
|
+
keyboardOpen.current = false;
|
|
40
|
+
setOpen(false);
|
|
41
|
+
}, [disabled]);
|
|
42
|
+
const closeSoon = () => {
|
|
43
|
+
cancelTimer();
|
|
44
|
+
if (keyboardOpen.current) return;
|
|
45
|
+
timer.current = setTimeout(() => setOpen(false), 200);
|
|
46
|
+
};
|
|
47
|
+
const options = [
|
|
48
|
+
{ value: false, label: t('unchecked'), Icon: Square },
|
|
49
|
+
{ value: 'indeterminate', label: t('in_progress'), Icon: Minus },
|
|
50
|
+
{ value: true, label: t('completed'), Icon: Check },
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Popover open={open && !disabled} onOpenChange={setOpen}>
|
|
55
|
+
<PopoverAnchor asChild>
|
|
56
|
+
<div
|
|
57
|
+
ref={anchor}
|
|
58
|
+
onPointerEnter={(event) => {
|
|
59
|
+
if (disabled || event.pointerType === 'touch') return;
|
|
60
|
+
cancelTimer();
|
|
61
|
+
keyboardOpen.current = false;
|
|
62
|
+
timer.current = setTimeout(() => setOpen(true), 500);
|
|
63
|
+
}}
|
|
64
|
+
onContextMenu={(event) => {
|
|
65
|
+
if (disabled) return;
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
cancelTimer();
|
|
68
|
+
keyboardOpen.current = true;
|
|
69
|
+
setOpen(true);
|
|
70
|
+
}}
|
|
71
|
+
onPointerLeave={closeSoon}
|
|
72
|
+
onKeyDown={(event) => {
|
|
73
|
+
if (disabled || event.key !== 'ArrowDown') return;
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
event.stopPropagation();
|
|
76
|
+
cancelTimer();
|
|
77
|
+
keyboardOpen.current = true;
|
|
78
|
+
setOpen(true);
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{children}
|
|
82
|
+
</div>
|
|
83
|
+
</PopoverAnchor>
|
|
84
|
+
<PopoverContent
|
|
85
|
+
side="top"
|
|
86
|
+
align="start"
|
|
87
|
+
sideOffset={6}
|
|
88
|
+
className="w-auto max-w-[calc(100vw-2rem)] p-1"
|
|
89
|
+
contentEditable={false}
|
|
90
|
+
aria-label={t('status')}
|
|
91
|
+
onPointerEnter={cancelTimer}
|
|
92
|
+
onPointerLeave={closeSoon}
|
|
93
|
+
onFocusCapture={cancelTimer}
|
|
94
|
+
onInteractOutside={() => {
|
|
95
|
+
keyboardOpen.current = false;
|
|
96
|
+
cancelTimer();
|
|
97
|
+
}}
|
|
98
|
+
onOpenAutoFocus={(event) => {
|
|
99
|
+
if (!keyboardOpen.current) event.preventDefault();
|
|
100
|
+
}}
|
|
101
|
+
onCloseAutoFocus={(event) => {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
if (keyboardOpen.current)
|
|
104
|
+
anchor.current?.querySelector('button')?.focus();
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<div className="flex items-center gap-0.5">
|
|
108
|
+
{options.map(({ value, label, Icon }) => (
|
|
109
|
+
<button
|
|
110
|
+
key={String(value)}
|
|
111
|
+
type="button"
|
|
112
|
+
aria-label={label}
|
|
113
|
+
aria-pressed={state === value}
|
|
114
|
+
className="flex min-h-9 items-center gap-1.5 rounded-sm px-2 text-muted-foreground text-xs hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-pressed:bg-accent aria-pressed:text-accent-foreground"
|
|
115
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
116
|
+
onClick={() => {
|
|
117
|
+
onSelect(value);
|
|
118
|
+
cancelTimer();
|
|
119
|
+
setOpen(false);
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<Icon className="size-3.5 shrink-0" />
|
|
123
|
+
{label}
|
|
124
|
+
</button>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
</PopoverContent>
|
|
128
|
+
</Popover>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -50,7 +50,7 @@ function renderCalendarSync({
|
|
|
50
50
|
</QueryClientProvider>
|
|
51
51
|
);
|
|
52
52
|
|
|
53
|
-
return renderHook(() => useCalendarSync(), { wrapper });
|
|
53
|
+
return { ...renderHook(() => useCalendarSync(), { wrapper }), queryClient };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function mockCalendarFetch(
|
|
@@ -570,3 +570,22 @@ describe('CalendarSyncProvider optimistic visible events', () => {
|
|
|
570
570
|
});
|
|
571
571
|
});
|
|
572
572
|
});
|
|
573
|
+
|
|
574
|
+
it('refetches invalidated event ranges even while the local range cache is fresh', async () => {
|
|
575
|
+
const fetchMock = mockCalendarFetch([createEvent('before')]);
|
|
576
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
577
|
+
const { result, queryClient } = renderCalendarSync();
|
|
578
|
+
act(() => result.current.setDates(createWeekDates('2026-06-22')));
|
|
579
|
+
await waitFor(() =>
|
|
580
|
+
expect(result.current.events.map((event) => event.id)).toEqual(['before'])
|
|
581
|
+
);
|
|
582
|
+
fetchMock.mockImplementation(mockCalendarFetch([createEvent('after')]));
|
|
583
|
+
await act(() =>
|
|
584
|
+
queryClient.invalidateQueries({
|
|
585
|
+
queryKey: ['databaseCalendarEvents', 'workspace-1'],
|
|
586
|
+
})
|
|
587
|
+
);
|
|
588
|
+
await waitFor(() =>
|
|
589
|
+
expect(result.current.events.map((event) => event.id)).toEqual(['after'])
|
|
590
|
+
);
|
|
591
|
+
});
|