@tuturuuu/ui 0.8.0 → 0.9.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.
Files changed (182) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/biome.json +1 -1
  3. package/package.json +73 -71
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  29. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  30. package/src/components/ui/custom/combobox.test.tsx +195 -0
  31. package/src/components/ui/custom/combobox.tsx +273 -156
  32. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  33. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  34. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  35. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  36. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  37. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  38. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  39. package/src/components/ui/custom/workspace-select.tsx +8 -3
  40. package/src/components/ui/dialog.test.tsx +52 -0
  41. package/src/components/ui/dialog.tsx +6 -2
  42. package/src/components/ui/dropdown-menu.tsx +5 -1
  43. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  44. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  45. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  46. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  47. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  48. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  49. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  50. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  51. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  52. package/src/components/ui/finance/invoices/utils.ts +3 -1
  53. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  54. package/src/components/ui/finance/transactions/form-types.ts +1 -0
  55. package/src/components/ui/finance/transactions/form.tsx +2 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  57. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  58. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  59. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  60. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  61. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  62. package/src/components/ui/finance/wallets/form.tsx +15 -4
  63. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  64. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  65. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  66. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  67. package/src/components/ui/input-otp.tsx +1 -1
  68. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  69. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  70. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  71. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  72. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  73. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  74. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  75. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  76. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  77. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  78. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  79. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  80. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  81. package/src/components/ui/navigation-menu.tsx +1 -1
  82. package/src/components/ui/pagination.tsx +1 -1
  83. package/src/components/ui/radio-group.tsx +1 -1
  84. package/src/components/ui/select.tsx +5 -1
  85. package/src/components/ui/sheet.tsx +1 -1
  86. package/src/components/ui/sidebar.tsx +1 -1
  87. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  88. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  89. package/src/components/ui/storefront/cart-summary.tsx +93 -154
  90. package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
  91. package/src/components/ui/storefront/listing-card.tsx +1 -1
  92. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  93. package/src/components/ui/storefront/product-detail.tsx +1 -1
  94. package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
  95. package/src/components/ui/storefront/storefront-surface.tsx +101 -166
  96. package/src/components/ui/storefront/types.ts +4 -0
  97. package/src/components/ui/storefront/utils.ts +6 -0
  98. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  99. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  100. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  101. package/src/components/ui/text-editor/editor.tsx +69 -14
  102. package/src/components/ui/text-editor/extensions.ts +8 -2
  103. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  104. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  105. package/src/components/ui/toast.tsx +1 -1
  106. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  107. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  108. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  109. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
  110. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  111. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  112. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  113. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  114. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  115. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  116. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
  117. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  118. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  119. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
  120. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  121. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  122. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  123. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  124. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  125. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  126. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  127. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  128. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  129. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  130. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  131. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
  132. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
  133. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  134. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  135. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  136. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
  137. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
  138. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  139. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  140. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  141. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  142. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  143. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  144. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  145. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  146. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  147. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  148. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  149. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  150. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  151. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  152. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  153. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  154. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
  155. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  156. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  157. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  158. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  159. package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
  160. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  161. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  162. package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
  163. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  164. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  165. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  166. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  167. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  168. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  169. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  170. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  171. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  172. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
  173. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  174. package/src/declarations.d.ts +1 -0
  175. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  176. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  177. package/src/hooks/use-calendar-sync.tsx +247 -243
  178. package/src/hooks/use-calendar.tsx +323 -138
  179. package/src/hooks/use-task-actions.ts +24 -0
  180. package/src/hooks/use-user-workspace-config.ts +75 -0
  181. package/src/hooks/use-workspace-currency.ts +8 -3
  182. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
@@ -0,0 +1,251 @@
1
+ 'use client';
2
+
3
+ import { Loader2, Plus, Save, Share2 } from '@tuturuuu/icons';
4
+ import type {
5
+ TaskPlan,
6
+ TaskPlanPeriod,
7
+ TaskPlanStatus,
8
+ } from '@tuturuuu/internal-api';
9
+ import { Badge } from '@tuturuuu/ui/badge';
10
+ import { Button } from '@tuturuuu/ui/button';
11
+ import { Combobox } from '@tuturuuu/ui/custom/combobox';
12
+ import { Input } from '@tuturuuu/ui/input';
13
+ import { useTranslations } from 'next-intl';
14
+ import {
15
+ getPlanWindowLabel,
16
+ TASK_PLAN_PERIODS,
17
+ TASK_PLAN_STATUSES,
18
+ } from './planner-utils';
19
+
20
+ interface PlannerPlanToolbarProps {
21
+ createPending: boolean;
22
+ mode: TaskPlanPeriod;
23
+ onCreatePlan: () => void;
24
+ onModeChange: (mode: TaskPlanPeriod) => void;
25
+ onPlanTitleChange: (title: string) => void;
26
+ onSelectedPlanChange: (planId: string) => void;
27
+ onSharePlan?: () => void;
28
+ planTitle: string;
29
+ plans: TaskPlan[];
30
+ plansLoading: boolean;
31
+ selectedPlan: TaskPlan | null;
32
+ }
33
+
34
+ type PlannerPlanBrowserProps = Pick<
35
+ PlannerPlanToolbarProps,
36
+ | 'onSelectedPlanChange'
37
+ | 'onSharePlan'
38
+ | 'plans'
39
+ | 'plansLoading'
40
+ | 'selectedPlan'
41
+ >;
42
+
43
+ function PlannerPlanBrowser({
44
+ onSelectedPlanChange,
45
+ onSharePlan,
46
+ plans,
47
+ plansLoading,
48
+ selectedPlan,
49
+ }: PlannerPlanBrowserProps) {
50
+ const t = useTranslations('ws-task-plans');
51
+ const planOptions = plans.map((plan) => ({
52
+ value: plan.id,
53
+ label: plan.title,
54
+ description: getPlanWindowLabel(plan),
55
+ }));
56
+
57
+ return (
58
+ <div className="grid gap-2 md:grid-cols-[minmax(0,1fr)_auto] md:items-start">
59
+ <Combobox
60
+ mode="single"
61
+ options={planOptions}
62
+ selected={selectedPlan?.id ?? ''}
63
+ onChange={(value) => onSelectedPlanChange(value as string)}
64
+ placeholder={t('plan_switcher_placeholder')}
65
+ searchPlaceholder={t('plan_switcher_placeholder')}
66
+ emptyText={t('no_plans')}
67
+ disabled={plansLoading || plans.length === 0}
68
+ className="[&_button]:h-9"
69
+ />
70
+
71
+ {onSharePlan && selectedPlan && (
72
+ <Button
73
+ type="button"
74
+ variant="outline"
75
+ size="sm"
76
+ onClick={onSharePlan}
77
+ className="h-9 gap-2"
78
+ >
79
+ <Share2 className="h-4 w-4" />
80
+ {t('share_plan')}
81
+ </Button>
82
+ )}
83
+
84
+ {selectedPlan ? (
85
+ <div className="flex flex-wrap items-center gap-2 md:col-span-2">
86
+ <Badge variant="outline">{getPlanWindowLabel(selectedPlan)}</Badge>
87
+ <Badge variant="secondary">
88
+ {t(`mode_${selectedPlan.period_type}`)}
89
+ </Badge>
90
+ <Badge variant="secondary">
91
+ {t(`plan_status_${selectedPlan.status}`)}
92
+ </Badge>
93
+ <Badge variant="outline">
94
+ {selectedPlan.items?.length ?? 0} {t('planned_tasks')}
95
+ </Badge>
96
+ <Badge variant="outline">
97
+ {selectedPlan.shares?.length ?? 0} {t('share_plan')}
98
+ </Badge>
99
+ </div>
100
+ ) : (
101
+ <div className="rounded-md border border-dashed p-3 text-muted-foreground text-sm md:col-span-2">
102
+ {plansLoading ? t('loading') : t('no_plans')}
103
+ </div>
104
+ )}
105
+ </div>
106
+ );
107
+ }
108
+
109
+ interface PlannerCreatePlanPanelProps {
110
+ createPending: boolean;
111
+ mode: TaskPlanPeriod;
112
+ onCreatePlan: () => void;
113
+ onModeChange: (mode: TaskPlanPeriod) => void;
114
+ onPlanTitleChange: (title: string) => void;
115
+ planTitle: string;
116
+ }
117
+
118
+ export function PlannerCreatePlanPanel({
119
+ createPending,
120
+ mode,
121
+ onCreatePlan,
122
+ onModeChange,
123
+ onPlanTitleChange,
124
+ planTitle,
125
+ }: PlannerCreatePlanPanelProps) {
126
+ const t = useTranslations('ws-task-plans');
127
+ const periodOptions = TASK_PLAN_PERIODS.map((period) => ({
128
+ value: period,
129
+ label: t(`mode_${period}`),
130
+ }));
131
+
132
+ return (
133
+ <div className="grid min-w-0 gap-2 md:grid-cols-[9rem_minmax(0,1fr)_auto]">
134
+ <Combobox
135
+ mode="single"
136
+ options={periodOptions}
137
+ selected={mode}
138
+ onChange={(value) => onModeChange(value as TaskPlanPeriod)}
139
+ placeholder={t('mode_week')}
140
+ searchPlaceholder={t('planner')}
141
+ className="[&_button]:h-9"
142
+ />
143
+ <Input
144
+ value={planTitle}
145
+ onChange={(event) => onPlanTitleChange(event.target.value)}
146
+ placeholder={t('plan_title_placeholder')}
147
+ className="h-9 min-w-0"
148
+ />
149
+ <Button
150
+ type="button"
151
+ onClick={onCreatePlan}
152
+ disabled={createPending}
153
+ size="sm"
154
+ className="h-9 gap-2"
155
+ >
156
+ {createPending ? (
157
+ <Loader2 className="h-4 w-4 animate-spin" />
158
+ ) : (
159
+ <Plus className="h-4 w-4" />
160
+ )}
161
+ {t('create_plan')}
162
+ </Button>
163
+ </div>
164
+ );
165
+ }
166
+
167
+ interface PlannerEditPlanPanelProps {
168
+ editMode: TaskPlanPeriod;
169
+ editStatus: TaskPlanStatus;
170
+ editTitle: string;
171
+ onEditModeChange: (mode: TaskPlanPeriod) => void;
172
+ onEditStatusChange: (status: TaskPlanStatus) => void;
173
+ onEditTitleChange: (title: string) => void;
174
+ onSavePlan: () => void;
175
+ savePending: boolean;
176
+ }
177
+
178
+ export function PlannerEditPlanPanel({
179
+ editMode,
180
+ editStatus,
181
+ editTitle,
182
+ onEditModeChange,
183
+ onEditStatusChange,
184
+ onEditTitleChange,
185
+ onSavePlan,
186
+ savePending,
187
+ }: PlannerEditPlanPanelProps) {
188
+ const t = useTranslations('ws-task-plans');
189
+ const periodOptions = TASK_PLAN_PERIODS.map((period) => ({
190
+ value: period,
191
+ label: t(`mode_${period}`),
192
+ }));
193
+ const statusOptions = TASK_PLAN_STATUSES.map((status) => ({
194
+ value: status,
195
+ label: t(`plan_status_${status}`),
196
+ }));
197
+
198
+ return (
199
+ <div className="grid min-w-0 gap-2 md:grid-cols-[minmax(0,1fr)_9rem_9rem_auto]">
200
+ <Input
201
+ value={editTitle}
202
+ onChange={(event) => onEditTitleChange(event.target.value)}
203
+ placeholder={t('plan_title_placeholder')}
204
+ className="h-9 min-w-0"
205
+ />
206
+ <Combobox
207
+ mode="single"
208
+ options={periodOptions}
209
+ selected={editMode}
210
+ onChange={(value) => onEditModeChange(value as TaskPlanPeriod)}
211
+ placeholder={t('mode_week')}
212
+ searchPlaceholder={t('planner')}
213
+ className="[&_button]:h-9"
214
+ />
215
+ <Combobox
216
+ mode="single"
217
+ options={statusOptions}
218
+ selected={editStatus}
219
+ onChange={(value) => onEditStatusChange(value as TaskPlanStatus)}
220
+ placeholder={t('plan_status_draft')}
221
+ searchPlaceholder={t('planner')}
222
+ className="[&_button]:h-9"
223
+ />
224
+ <Button
225
+ type="button"
226
+ onClick={onSavePlan}
227
+ disabled={savePending}
228
+ size="sm"
229
+ className="h-9 gap-2"
230
+ >
231
+ {savePending ? (
232
+ <Loader2 className="h-4 w-4 animate-spin" />
233
+ ) : (
234
+ <Save className="h-4 w-4" />
235
+ )}
236
+ {t('save_plan')}
237
+ </Button>
238
+ </div>
239
+ );
240
+ }
241
+
242
+ export function PlannerPlanToolbar(props: PlannerPlanToolbarProps) {
243
+ return (
244
+ <div className="grid gap-3">
245
+ <PlannerPlanBrowser {...props} />
246
+ <PlannerCreatePlanPanel {...props} />
247
+ </div>
248
+ );
249
+ }
250
+
251
+ export { PlannerPlanBrowser };
@@ -0,0 +1,27 @@
1
+ import type { TaskPlanItem } from '@tuturuuu/internal-api';
2
+ import { Badge } from '@tuturuuu/ui/badge';
3
+ import { useTranslations } from 'next-intl';
4
+ import { getTaskPlanItemScope } from './planner-utils';
5
+
6
+ interface PlannerScopeBadgeProps {
7
+ item: TaskPlanItem;
8
+ personalWorkspaceId: string;
9
+ }
10
+
11
+ export function PlannerScopeBadge({
12
+ item,
13
+ personalWorkspaceId,
14
+ }: PlannerScopeBadgeProps) {
15
+ const t = useTranslations('ws-task-plans');
16
+ const scope = getTaskPlanItemScope(item, personalWorkspaceId);
17
+
18
+ if (scope === 'draft') {
19
+ return <Badge variant="secondary">{t('scope_draft')}</Badge>;
20
+ }
21
+
22
+ if (scope === 'personal') {
23
+ return <Badge variant="outline">{t('scope_personal')}</Badge>;
24
+ }
25
+
26
+ return <Badge>{t('scope_external_workspace')}</Badge>;
27
+ }
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import { ChevronDown } from '@tuturuuu/icons';
4
+ import {
5
+ Collapsible,
6
+ CollapsibleContent,
7
+ CollapsibleTrigger,
8
+ } from '@tuturuuu/ui/collapsible';
9
+ import { cn } from '@tuturuuu/utils/format';
10
+ import { type ReactNode, useState } from 'react';
11
+
12
+ interface PlannerSectionProps {
13
+ badge?: ReactNode;
14
+ children: ReactNode;
15
+ defaultOpen?: boolean;
16
+ disabled?: boolean;
17
+ title: string;
18
+ }
19
+
20
+ export function PlannerSection({
21
+ badge,
22
+ children,
23
+ defaultOpen = false,
24
+ disabled,
25
+ title,
26
+ }: PlannerSectionProps) {
27
+ const [open, setOpen] = useState(defaultOpen);
28
+
29
+ return (
30
+ <Collapsible
31
+ open={open}
32
+ onOpenChange={setOpen}
33
+ className="rounded-md border bg-background"
34
+ >
35
+ <CollapsibleTrigger asChild>
36
+ <button
37
+ type="button"
38
+ disabled={disabled}
39
+ className="flex min-h-11 w-full items-center justify-between gap-3 px-3 text-left transition-colors hover:bg-muted/40 disabled:cursor-not-allowed disabled:opacity-60"
40
+ >
41
+ <span className="flex min-w-0 items-center gap-2">
42
+ <span className="truncate font-medium text-sm">{title}</span>
43
+ {badge}
44
+ </span>
45
+ <ChevronDown
46
+ className={cn(
47
+ 'h-4 w-4 shrink-0 text-muted-foreground transition-transform',
48
+ open && 'rotate-180'
49
+ )}
50
+ />
51
+ </button>
52
+ </CollapsibleTrigger>
53
+ <CollapsibleContent className="border-t p-3">
54
+ {children}
55
+ </CollapsibleContent>
56
+ </Collapsible>
57
+ );
58
+ }
@@ -0,0 +1,238 @@
1
+ 'use client';
2
+
3
+ import { useMutation } from '@tanstack/react-query';
4
+ import { Loader2, Share2 } from '@tuturuuu/icons';
5
+ import {
6
+ createWorkspaceTaskPlanShare,
7
+ isTaskPlanSchemaUnavailable,
8
+ type TaskPlan,
9
+ type TaskPlanPermission,
10
+ } from '@tuturuuu/internal-api';
11
+ import { Button } from '@tuturuuu/ui/button';
12
+ import { Combobox } from '@tuturuuu/ui/custom/combobox';
13
+ import {
14
+ Dialog,
15
+ DialogContent,
16
+ DialogFooter,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ } from '@tuturuuu/ui/dialog';
20
+ import { Input } from '@tuturuuu/ui/input';
21
+ import { toast } from '@tuturuuu/ui/sonner';
22
+ import { useTranslations } from 'next-intl';
23
+ import { useState } from 'react';
24
+
25
+ interface PlannerShareDialogProps {
26
+ onOpenChange: (open: boolean) => void;
27
+ onShared: () => void;
28
+ open: boolean;
29
+ plan: TaskPlan;
30
+ targetWorkspaceId: string | null;
31
+ targetWorkspaceName: string | null;
32
+ workspaceId: string;
33
+ }
34
+
35
+ export function PlannerShareDialog({
36
+ onOpenChange,
37
+ onShared,
38
+ open,
39
+ plan,
40
+ targetWorkspaceId,
41
+ targetWorkspaceName,
42
+ workspaceId,
43
+ }: PlannerShareDialogProps) {
44
+ const t = useTranslations('ws-task-plans');
45
+ const tCommon = useTranslations('common');
46
+ const [email, setEmail] = useState('');
47
+ const [permission, setPermission] = useState<TaskPlanPermission>('view');
48
+ const permissionOptions = [
49
+ { value: 'view', label: t('permission_view') },
50
+ { value: 'edit', label: t('permission_edit') },
51
+ ];
52
+
53
+ const shareMutation = useMutation({
54
+ mutationFn: (recipient: 'email' | 'workspace') => {
55
+ if (recipient === 'workspace' && targetWorkspaceId) {
56
+ return createWorkspaceTaskPlanShare(workspaceId, plan.id, {
57
+ shared_with_ws_id: targetWorkspaceId,
58
+ permission,
59
+ });
60
+ }
61
+
62
+ return createWorkspaceTaskPlanShare(workspaceId, plan.id, {
63
+ shared_with_email: email.trim(),
64
+ permission,
65
+ });
66
+ },
67
+ onSuccess: (response) => {
68
+ if (isTaskPlanSchemaUnavailable(response)) {
69
+ toast.error(t('schema_unavailable'));
70
+ return;
71
+ }
72
+
73
+ setEmail('');
74
+ onShared();
75
+ toast.success(t('share_saved'));
76
+ },
77
+ onError: () => toast.error(tCommon('error')),
78
+ });
79
+
80
+ return (
81
+ <Dialog open={open} onOpenChange={onOpenChange}>
82
+ <DialogContent className="sm:max-w-lg">
83
+ <DialogHeader>
84
+ <DialogTitle>{t('share_plan')}</DialogTitle>
85
+ </DialogHeader>
86
+
87
+ <div className="space-y-3">
88
+ <div className="grid grid-cols-[1fr_8rem] gap-2">
89
+ <Input
90
+ value={email}
91
+ onChange={(event) => setEmail(event.target.value)}
92
+ placeholder={t('share_email_placeholder')}
93
+ type="email"
94
+ />
95
+ <Combobox
96
+ mode="single"
97
+ options={permissionOptions}
98
+ selected={permission}
99
+ onChange={(value) => setPermission(value as TaskPlanPermission)}
100
+ placeholder={t('permission_view')}
101
+ />
102
+ </div>
103
+
104
+ <div className="flex flex-col gap-2 sm:flex-row">
105
+ <Button
106
+ type="button"
107
+ onClick={() => shareMutation.mutate('email')}
108
+ disabled={!email.trim() || shareMutation.isPending}
109
+ className="gap-2"
110
+ >
111
+ {shareMutation.isPending ? (
112
+ <Loader2 className="h-4 w-4 animate-spin" />
113
+ ) : (
114
+ <Share2 className="h-4 w-4" />
115
+ )}
116
+ {t('share_email')}
117
+ </Button>
118
+ <Button
119
+ type="button"
120
+ variant="outline"
121
+ onClick={() => shareMutation.mutate('workspace')}
122
+ disabled={!targetWorkspaceId || shareMutation.isPending}
123
+ >
124
+ {targetWorkspaceName
125
+ ? t('share_workspace_name', { name: targetWorkspaceName })
126
+ : t('share_workspace')}
127
+ </Button>
128
+ </div>
129
+ </div>
130
+
131
+ <DialogFooter>
132
+ <Button variant="ghost" onClick={() => onOpenChange(false)}>
133
+ {tCommon('close')}
134
+ </Button>
135
+ </DialogFooter>
136
+ </DialogContent>
137
+ </Dialog>
138
+ );
139
+ }
140
+
141
+ interface PlannerSharePanelProps {
142
+ onShared: () => void;
143
+ plan: TaskPlan;
144
+ targetWorkspaceId: string | null;
145
+ targetWorkspaceName: string | null;
146
+ workspaceId: string;
147
+ }
148
+
149
+ export function PlannerSharePanel({
150
+ onShared,
151
+ plan,
152
+ targetWorkspaceId,
153
+ targetWorkspaceName,
154
+ workspaceId,
155
+ }: PlannerSharePanelProps) {
156
+ const t = useTranslations('ws-task-plans');
157
+ const tCommon = useTranslations('common');
158
+ const [email, setEmail] = useState('');
159
+ const [permission, setPermission] = useState<TaskPlanPermission>('view');
160
+ const permissionOptions = [
161
+ { value: 'view', label: t('permission_view') },
162
+ { value: 'edit', label: t('permission_edit') },
163
+ ];
164
+
165
+ const shareMutation = useMutation({
166
+ mutationFn: (recipient: 'email' | 'workspace') => {
167
+ if (recipient === 'workspace' && targetWorkspaceId) {
168
+ return createWorkspaceTaskPlanShare(workspaceId, plan.id, {
169
+ shared_with_ws_id: targetWorkspaceId,
170
+ permission,
171
+ });
172
+ }
173
+
174
+ return createWorkspaceTaskPlanShare(workspaceId, plan.id, {
175
+ shared_with_email: email.trim(),
176
+ permission,
177
+ });
178
+ },
179
+ onSuccess: (response) => {
180
+ if (isTaskPlanSchemaUnavailable(response)) {
181
+ toast.error(t('schema_unavailable'));
182
+ return;
183
+ }
184
+
185
+ setEmail('');
186
+ onShared();
187
+ toast.success(t('share_saved'));
188
+ },
189
+ onError: () => toast.error(tCommon('error')),
190
+ });
191
+
192
+ return (
193
+ <div className="space-y-2">
194
+ <div className="grid gap-2 sm:grid-cols-[1fr_8rem_auto]">
195
+ <Input
196
+ value={email}
197
+ onChange={(event) => setEmail(event.target.value)}
198
+ placeholder={t('share_email_placeholder')}
199
+ type="email"
200
+ className="h-9"
201
+ />
202
+ <Combobox
203
+ mode="single"
204
+ options={permissionOptions}
205
+ selected={permission}
206
+ onChange={(value) => setPermission(value as TaskPlanPermission)}
207
+ placeholder={t('permission_view')}
208
+ className="[&_button]:h-9"
209
+ />
210
+ <Button
211
+ type="button"
212
+ onClick={() => shareMutation.mutate('email')}
213
+ disabled={!email.trim() || shareMutation.isPending}
214
+ className="h-9 gap-2"
215
+ >
216
+ {shareMutation.isPending ? (
217
+ <Loader2 className="h-4 w-4 animate-spin" />
218
+ ) : (
219
+ <Share2 className="h-4 w-4" />
220
+ )}
221
+ {t('share_email')}
222
+ </Button>
223
+ </div>
224
+
225
+ <Button
226
+ type="button"
227
+ variant="outline"
228
+ onClick={() => shareMutation.mutate('workspace')}
229
+ disabled={!targetWorkspaceId || shareMutation.isPending}
230
+ className="h-9"
231
+ >
232
+ {targetWorkspaceName
233
+ ? t('share_workspace_name', { name: targetWorkspaceName })
234
+ : t('share_workspace')}
235
+ </Button>
236
+ </div>
237
+ );
238
+ }