@tuturuuu/ui 0.7.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 (226) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/biome.json +1 -1
  3. package/package.json +75 -73
  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/currency-input.test.tsx +43 -0
  29. package/src/components/ui/currency-input.tsx +1 -1
  30. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  31. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  32. package/src/components/ui/custom/combobox.test.tsx +195 -0
  33. package/src/components/ui/custom/combobox.tsx +273 -156
  34. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  35. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  36. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  37. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  38. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  39. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  40. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  41. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  42. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  43. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  44. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  45. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  46. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  47. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  48. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  49. package/src/components/ui/custom/workspace-select.tsx +8 -3
  50. package/src/components/ui/dialog.test.tsx +52 -0
  51. package/src/components/ui/dialog.tsx +6 -2
  52. package/src/components/ui/dropdown-menu.tsx +5 -1
  53. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  54. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  55. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  56. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  57. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  58. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  59. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  60. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  61. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  62. package/src/components/ui/finance/invoices/utils.ts +3 -1
  63. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  64. package/src/components/ui/finance/transactions/form-types.ts +3 -0
  65. package/src/components/ui/finance/transactions/form.tsx +2 -0
  66. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  67. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  68. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  69. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  70. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  71. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  72. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  73. package/src/components/ui/finance/wallets/form.tsx +15 -4
  74. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  75. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  76. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  77. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  78. package/src/components/ui/input-otp.tsx +1 -1
  79. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  80. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  81. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  82. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  83. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  84. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  85. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  86. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  87. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  88. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  89. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  90. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  91. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  92. package/src/components/ui/money-input.test.tsx +64 -0
  93. package/src/components/ui/money-input.tsx +63 -0
  94. package/src/components/ui/navigation-menu.tsx +1 -1
  95. package/src/components/ui/pagination.tsx +1 -1
  96. package/src/components/ui/radio-group.tsx +1 -1
  97. package/src/components/ui/select.tsx +5 -1
  98. package/src/components/ui/sheet.tsx +1 -1
  99. package/src/components/ui/sidebar.tsx +1 -1
  100. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  101. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  102. package/src/components/ui/storefront/cart-summary.tsx +104 -80
  103. package/src/components/ui/storefront/checkout-overlay.tsx +26 -0
  104. package/src/components/ui/storefront/hero-panel.tsx +2 -8
  105. package/src/components/ui/storefront/image-panel.tsx +6 -0
  106. package/src/components/ui/storefront/index.ts +11 -0
  107. package/src/components/ui/storefront/listing-card.tsx +84 -22
  108. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  109. package/src/components/ui/storefront/product-detail.tsx +289 -0
  110. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  111. package/src/components/ui/storefront/storefront-surface.test.tsx +221 -3
  112. package/src/components/ui/storefront/storefront-surface.tsx +288 -153
  113. package/src/components/ui/storefront/types.ts +27 -1
  114. package/src/components/ui/storefront/utils.ts +117 -27
  115. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  116. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  117. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  118. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  119. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  120. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  121. package/src/components/ui/text-editor/content-migration.ts +41 -18
  122. package/src/components/ui/text-editor/editor.tsx +69 -14
  123. package/src/components/ui/text-editor/extensions.ts +9 -3
  124. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  125. package/src/components/ui/text-editor/image-extension.ts +40 -18
  126. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  127. package/src/components/ui/text-editor/video-extension.ts +11 -2
  128. package/src/components/ui/toast.tsx +1 -1
  129. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  130. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  131. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  132. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  133. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  134. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +113 -46
  135. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  136. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  137. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  138. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  139. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  140. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  141. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +51 -9
  142. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  143. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  144. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  145. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +127 -38
  146. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  147. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  148. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  149. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  150. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  151. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  152. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  153. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  154. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  155. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  156. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  157. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +410 -4
  158. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +106 -14
  159. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  160. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  161. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  162. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +186 -0
  163. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +59 -2
  164. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  165. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  166. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  167. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  168. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  169. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  170. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  171. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  172. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  173. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  174. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  175. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  176. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  177. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  178. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  179. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  180. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  181. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  182. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  183. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  184. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  185. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  186. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +237 -3
  187. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  188. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  189. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  190. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  191. package/src/components/ui/tu-do/shared/board-header.tsx +465 -937
  192. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  193. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  194. package/src/components/ui/tu-do/shared/board-views.tsx +596 -82
  195. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  196. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  197. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  198. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  199. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  200. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  201. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  202. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  203. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  204. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  205. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  206. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  207. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  208. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  209. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  210. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +44 -15
  211. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  212. package/src/declarations.d.ts +1 -0
  213. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  214. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  215. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  216. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  217. package/src/hooks/use-calendar-sync.tsx +247 -243
  218. package/src/hooks/use-calendar.tsx +323 -138
  219. package/src/hooks/use-task-actions.ts +24 -0
  220. package/src/hooks/use-user-workspace-config.ts +75 -0
  221. package/src/hooks/use-workspace-currency.ts +8 -3
  222. package/src/hooks/useBoardRealtime.ts +6 -3
  223. package/src/hooks/useBoardRealtime.types.ts +11 -0
  224. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
  225. package/src/hooks/useCursorTracking.ts +91 -27
  226. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -0,0 +1,231 @@
1
+ 'use client';
2
+
3
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
+ import {
5
+ ChevronDown,
6
+ Copy,
7
+ ExternalLink,
8
+ Globe2,
9
+ Info,
10
+ Loader2,
11
+ Trash2,
12
+ } from '@tuturuuu/icons';
13
+ import {
14
+ disableWorkspaceTaskBoardPublicLink,
15
+ enableWorkspaceTaskBoardPublicLink,
16
+ getWorkspaceTaskBoardPublicLink,
17
+ type WorkspaceTaskBoardPublicLink,
18
+ } from '@tuturuuu/internal-api/tasks';
19
+ import { Badge } from '@tuturuuu/ui/badge';
20
+ import { Button } from '@tuturuuu/ui/button';
21
+ import {
22
+ Collapsible,
23
+ CollapsibleContent,
24
+ CollapsibleTrigger,
25
+ } from '@tuturuuu/ui/collapsible';
26
+ import { Input } from '@tuturuuu/ui/input';
27
+ import {
28
+ Tooltip,
29
+ TooltipContent,
30
+ TooltipProvider,
31
+ TooltipTrigger,
32
+ } from '@tuturuuu/ui/tooltip';
33
+ import { cn } from '@tuturuuu/utils/format';
34
+ import { useLocale, useTranslations } from 'next-intl';
35
+ import { useState } from 'react';
36
+ import { toast } from 'sonner';
37
+
38
+ interface BoardPublicLinkSectionProps {
39
+ boardId: string;
40
+ open: boolean;
41
+ wsId: string;
42
+ }
43
+
44
+ function buildPublicBoardUrl(
45
+ locale: string,
46
+ link: WorkspaceTaskBoardPublicLink | null
47
+ ) {
48
+ if (!link?.code) return '';
49
+
50
+ const path = `/${locale}/shared/task-boards/${link.code}`;
51
+ if (typeof window === 'undefined') return path;
52
+ return `${window.location.origin}${path}`;
53
+ }
54
+
55
+ export function BoardPublicLinkSection({
56
+ boardId,
57
+ open,
58
+ wsId,
59
+ }: BoardPublicLinkSectionProps) {
60
+ const t = useTranslations();
61
+ const locale = useLocale();
62
+ const queryClient = useQueryClient();
63
+ const [sectionOpen, setSectionOpen] = useState(false);
64
+ const queryKey = ['task-board-public-link', wsId, boardId] as const;
65
+
66
+ const publicLinkQuery = useQuery({
67
+ queryKey,
68
+ queryFn: () => getWorkspaceTaskBoardPublicLink(wsId, boardId),
69
+ enabled: open,
70
+ staleTime: 60_000,
71
+ });
72
+
73
+ const enableMutation = useMutation({
74
+ mutationFn: () => enableWorkspaceTaskBoardPublicLink(wsId, boardId),
75
+ onSuccess: () => {
76
+ void queryClient.invalidateQueries({ queryKey });
77
+ toast.success(t('ws-task-boards.share.public.enabled'));
78
+ },
79
+ onError: () => {
80
+ toast.error(t('ws-task-boards.share.public.enable_failed'));
81
+ },
82
+ });
83
+
84
+ const disableMutation = useMutation({
85
+ mutationFn: () => disableWorkspaceTaskBoardPublicLink(wsId, boardId),
86
+ onSuccess: () => {
87
+ void queryClient.invalidateQueries({ queryKey });
88
+ toast.success(t('ws-task-boards.share.public.disabled'));
89
+ },
90
+ onError: () => {
91
+ toast.error(t('ws-task-boards.share.public.disable_failed'));
92
+ },
93
+ });
94
+
95
+ const publicLink = publicLinkQuery.data?.publicLink ?? null;
96
+ const publicUrl = buildPublicBoardUrl(locale, publicLink);
97
+ const isMutating = enableMutation.isPending || disableMutation.isPending;
98
+ const statusBadge = publicLinkQuery.isLoading ? (
99
+ <Badge variant="secondary" className="gap-1 px-2 py-0.5 text-[10px]">
100
+ <Loader2 className="h-3 w-3 animate-spin" />
101
+ {t('common.loading')}
102
+ </Badge>
103
+ ) : publicLink ? (
104
+ <Badge className="bg-dynamic-green/10 px-2 py-0.5 text-[10px] text-dynamic-green">
105
+ {t('common.enabled')}
106
+ </Badge>
107
+ ) : (
108
+ <Badge variant="secondary" className="px-2 py-0.5 text-[10px]">
109
+ {t('common.disabled')}
110
+ </Badge>
111
+ );
112
+
113
+ async function copyPublicUrl() {
114
+ if (!publicUrl) return;
115
+
116
+ try {
117
+ await navigator.clipboard.writeText(publicUrl);
118
+ toast.success(t('ws-task-boards.share.public.copy_success'));
119
+ } catch {
120
+ toast.error(t('ws-task-boards.share.public.copy_failed'));
121
+ }
122
+ }
123
+
124
+ function openPublicUrl() {
125
+ if (!publicUrl || typeof window === 'undefined') return;
126
+ window.open(publicUrl, '_blank', 'noopener,noreferrer');
127
+ }
128
+
129
+ return (
130
+ <Collapsible
131
+ open={sectionOpen}
132
+ onOpenChange={setSectionOpen}
133
+ className="rounded-md border"
134
+ >
135
+ <div className="flex min-h-11 items-center gap-2 px-3">
136
+ <CollapsibleTrigger asChild>
137
+ <button
138
+ type="button"
139
+ className="flex min-w-0 flex-1 items-center gap-2 text-left transition-colors hover:text-foreground"
140
+ >
141
+ <Globe2 className="h-4 w-4 text-muted-foreground" />
142
+ <span className="min-w-0 flex-1 truncate font-medium text-sm">
143
+ {t('ws-task-boards.share.public.title')}
144
+ </span>
145
+ {statusBadge}
146
+ <ChevronDown
147
+ className={cn(
148
+ 'h-4 w-4 shrink-0 text-muted-foreground transition-transform',
149
+ sectionOpen && 'rotate-180'
150
+ )}
151
+ />
152
+ </button>
153
+ </CollapsibleTrigger>
154
+ <TooltipProvider delayDuration={0} skipDelayDuration={0}>
155
+ <Tooltip>
156
+ <TooltipTrigger asChild>
157
+ <button
158
+ type="button"
159
+ className="text-muted-foreground transition-colors hover:text-foreground"
160
+ aria-label={t('ws-task-boards.share.note')}
161
+ >
162
+ <Info className="h-3.5 w-3.5" />
163
+ </button>
164
+ </TooltipTrigger>
165
+ <TooltipContent className="max-w-xs">
166
+ {t('ws-task-boards.share.public.tooltip')}
167
+ </TooltipContent>
168
+ </Tooltip>
169
+ </TooltipProvider>
170
+ </div>
171
+
172
+ <CollapsibleContent className="border-t p-3">
173
+ {publicLinkQuery.isLoading ? (
174
+ <div className="flex items-center gap-2 rounded-md border border-dashed p-3 text-muted-foreground text-sm">
175
+ <Loader2 className="h-4 w-4 animate-spin" />
176
+ {t('common.loading')}
177
+ </div>
178
+ ) : publicLink ? (
179
+ <div className="grid gap-2 sm:grid-cols-[1fr_auto_auto_auto]">
180
+ <Input value={publicUrl} readOnly className="min-w-0" />
181
+ <Button
182
+ type="button"
183
+ variant="outline"
184
+ onClick={copyPublicUrl}
185
+ disabled={!publicUrl}
186
+ >
187
+ <Copy className="h-4 w-4" />
188
+ {t('ws-task-boards.share.public.copy')}
189
+ </Button>
190
+ <Button
191
+ type="button"
192
+ variant="outline"
193
+ onClick={openPublicUrl}
194
+ disabled={!publicUrl}
195
+ >
196
+ <ExternalLink className="h-4 w-4" />
197
+ {t('ws-task-boards.share.public.open')}
198
+ </Button>
199
+ <Button
200
+ type="button"
201
+ variant="outline"
202
+ onClick={() => disableMutation.mutate()}
203
+ disabled={isMutating}
204
+ >
205
+ {disableMutation.isPending ? (
206
+ <Loader2 className="h-4 w-4 animate-spin" />
207
+ ) : (
208
+ <Trash2 className="h-4 w-4" />
209
+ )}
210
+ {t('ws-task-boards.share.public.disable')}
211
+ </Button>
212
+ </div>
213
+ ) : (
214
+ <Button
215
+ type="button"
216
+ variant="outline"
217
+ onClick={() => enableMutation.mutate()}
218
+ disabled={isMutating}
219
+ >
220
+ {enableMutation.isPending ? (
221
+ <Loader2 className="h-4 w-4 animate-spin" />
222
+ ) : (
223
+ <Globe2 className="h-4 w-4" />
224
+ )}
225
+ {t('ws-task-boards.share.public.enable')}
226
+ </Button>
227
+ )}
228
+ </CollapsibleContent>
229
+ </Collapsible>
230
+ );
231
+ }
@@ -1,19 +1,22 @@
1
1
  'use client';
2
2
 
3
3
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
- import { Loader2, Share2, Trash2 } from '@tuturuuu/icons';
4
+ import { Loader2, Share2, Trash2, Users } from '@tuturuuu/icons';
5
5
  import {
6
6
  createWorkspaceTaskBoardShare,
7
7
  deleteWorkspaceTaskBoardShare,
8
8
  listWorkspaceTaskBoardShares,
9
+ listWorkspaceTaskBoardViewableMembers,
9
10
  updateWorkspaceTaskBoardShare,
10
11
  type WorkspaceTaskBoardShare,
11
12
  type WorkspaceTaskBoardSharePermission,
13
+ type WorkspaceTaskBoardViewableMember,
12
14
  } from '@tuturuuu/internal-api/tasks';
13
15
  import type { WorkspaceTaskBoard } from '@tuturuuu/types';
14
16
  import { Avatar, AvatarFallback, AvatarImage } from '@tuturuuu/ui/avatar';
15
17
  import { Badge } from '@tuturuuu/ui/badge';
16
18
  import { Button } from '@tuturuuu/ui/button';
19
+ import { Combobox } from '@tuturuuu/ui/custom/combobox';
17
20
  import {
18
21
  Dialog,
19
22
  DialogContent,
@@ -23,17 +26,12 @@ import {
23
26
  DialogTitle,
24
27
  } from '@tuturuuu/ui/dialog';
25
28
  import { Input } from '@tuturuuu/ui/input';
26
- import {
27
- Select,
28
- SelectContent,
29
- SelectItem,
30
- SelectTrigger,
31
- SelectValue,
32
- } from '@tuturuuu/ui/select';
33
29
  import { getInitials } from '@tuturuuu/utils/name-helper';
34
30
  import { useTranslations } from 'next-intl';
35
- import { useState } from 'react';
31
+ import { useRef, useState } from 'react';
36
32
  import { toast } from 'sonner';
33
+ import { BoardPublicLinkSection } from './board-public-link-section';
34
+ import { ShareSection } from './share-section';
37
35
 
38
36
  interface BoardShareDialogProps {
39
37
  board: Pick<WorkspaceTaskBoard, 'id' | 'name'>;
@@ -52,6 +50,15 @@ function shareDisplayName(share: WorkspaceTaskBoardShare) {
52
50
  );
53
51
  }
54
52
 
53
+ function viewableMemberDisplayName(member: WorkspaceTaskBoardViewableMember) {
54
+ return (
55
+ member.display_name ||
56
+ (member.handle ? `@${member.handle}` : null) ||
57
+ member.email ||
58
+ member.user_id
59
+ );
60
+ }
61
+
55
62
  export function BoardShareDialog({
56
63
  board,
57
64
  onOpenChange,
@@ -63,6 +70,9 @@ export function BoardShareDialog({
63
70
  const [email, setEmail] = useState('');
64
71
  const [permission, setPermission] =
65
72
  useState<WorkspaceTaskBoardSharePermission>('view');
73
+ const [membersOpen, setMembersOpen] = useState(false);
74
+ const [guestsOpen, setGuestsOpen] = useState(false);
75
+ const initialFocusRef = useRef<HTMLDivElement>(null);
66
76
 
67
77
  const queryKey = ['task-board-shares', wsId, board.id] as const;
68
78
  const sharesQuery = useQuery({
@@ -70,6 +80,12 @@ export function BoardShareDialog({
70
80
  queryFn: () => listWorkspaceTaskBoardShares(wsId, board.id),
71
81
  enabled: open,
72
82
  });
83
+ const viewableMembersQuery = useQuery({
84
+ queryKey: ['task-board-viewable-members', wsId, board.id] as const,
85
+ queryFn: () => listWorkspaceTaskBoardViewableMembers(wsId, board.id),
86
+ enabled: open && membersOpen,
87
+ staleTime: 60_000,
88
+ });
73
89
 
74
90
  const createMutation = useMutation({
75
91
  mutationFn: () =>
@@ -121,16 +137,59 @@ export function BoardShareDialog({
121
137
  });
122
138
 
123
139
  const shares = sharesQuery.data?.shares ?? [];
140
+ const membersCount = viewableMembersQuery.data?.members.length;
141
+ const membersStatusBadge = viewableMembersQuery.isLoading ? (
142
+ <Badge variant="secondary" className="gap-1 px-2 py-0.5 text-[10px]">
143
+ <Loader2 className="h-3 w-3 animate-spin" />
144
+ {t('common.loading')}
145
+ </Badge>
146
+ ) : typeof membersCount === 'number' ? (
147
+ <Badge variant="secondary" className="px-2 py-0.5 text-[10px]">
148
+ {membersCount}
149
+ </Badge>
150
+ ) : (
151
+ <Badge variant="outline" className="px-2 py-0.5 text-[10px]">
152
+ {t('common.workspace')}
153
+ </Badge>
154
+ );
155
+ const guestsStatusBadge = sharesQuery.isLoading ? (
156
+ <Badge variant="secondary" className="gap-1 px-2 py-0.5 text-[10px]">
157
+ <Loader2 className="h-3 w-3 animate-spin" />
158
+ {t('common.loading')}
159
+ </Badge>
160
+ ) : shares.length ? (
161
+ <Badge variant="secondary" className="px-2 py-0.5 text-[10px]">
162
+ {shares.length}
163
+ </Badge>
164
+ ) : (
165
+ <Badge variant="outline" className="px-2 py-0.5 text-[10px]">
166
+ {t('common.none')}
167
+ </Badge>
168
+ );
124
169
  const canSubmit =
125
170
  email.trim().length > 0 &&
126
171
  !createMutation.isPending &&
127
172
  !sharesQuery.isLoading;
173
+ const permissionOptions = [
174
+ {
175
+ value: 'view',
176
+ label: t('ws-task-boards.share.permission.view'),
177
+ },
178
+ {
179
+ value: 'edit',
180
+ label: t('ws-task-boards.share.permission.edit'),
181
+ },
182
+ ];
128
183
 
129
184
  return (
130
185
  <Dialog open={open} onOpenChange={onOpenChange}>
131
186
  <DialogContent
132
- className="sm:max-w-xl"
187
+ className="max-h-[min(88dvh,720px)] overflow-y-auto sm:max-w-xl"
133
188
  onClick={(e) => e.stopPropagation()}
189
+ onOpenAutoFocus={(event) => {
190
+ event.preventDefault();
191
+ initialFocusRef.current?.focus();
192
+ }}
134
193
  >
135
194
  <DialogHeader>
136
195
  <DialogTitle className="flex items-center gap-2">
@@ -139,134 +198,188 @@ export function BoardShareDialog({
139
198
  name: board.name || t('common.untitled'),
140
199
  })}
141
200
  </DialogTitle>
142
- <DialogDescription>
143
- {t('ws-task-boards.share.description')}
201
+ <DialogDescription className="sr-only">
202
+ {t('ws-task-boards.share.title', {
203
+ name: board.name || t('common.untitled'),
204
+ })}
144
205
  </DialogDescription>
145
206
  </DialogHeader>
207
+ <div ref={initialFocusRef} tabIndex={-1} className="sr-only" />
146
208
 
147
- <div className="space-y-4">
148
- <div className="rounded-md border bg-muted/30 p-3 text-muted-foreground text-sm">
149
- {t('ws-task-boards.share.guest_scope')}
150
- </div>
209
+ <div className="space-y-2">
210
+ <BoardPublicLinkSection boardId={board.id} open={open} wsId={wsId} />
151
211
 
152
- <div className="grid gap-2 sm:grid-cols-[1fr_8rem_auto]">
153
- <Input
154
- type="email"
155
- value={email}
156
- onChange={(event) => setEmail(event.target.value)}
157
- placeholder={t('ws-task-boards.share.email_placeholder')}
158
- />
159
- <Select
160
- value={permission}
161
- onValueChange={(value) =>
162
- setPermission(value as WorkspaceTaskBoardSharePermission)
163
- }
164
- >
165
- <SelectTrigger>
166
- <SelectValue />
167
- </SelectTrigger>
168
- <SelectContent>
169
- <SelectItem value="view">
170
- {t('ws-task-boards.share.permission.view')}
171
- </SelectItem>
172
- <SelectItem value="edit">
173
- {t('ws-task-boards.share.permission.edit')}
174
- </SelectItem>
175
- </SelectContent>
176
- </Select>
177
- <Button
178
- type="button"
179
- onClick={() => createMutation.mutate()}
180
- disabled={!canSubmit}
181
- >
182
- {createMutation.isPending ? (
212
+ <ShareSection
213
+ open={membersOpen}
214
+ onOpenChange={setMembersOpen}
215
+ title={t('ws-task-boards.share.workspace_members.title')}
216
+ tooltip={t('ws-task-boards.share.workspace_members.tooltip')}
217
+ icon={<Users className="h-4 w-4 text-muted-foreground" />}
218
+ statusBadge={membersStatusBadge}
219
+ >
220
+ {viewableMembersQuery.isLoading ? (
221
+ <div className="flex items-center gap-2 text-muted-foreground text-sm">
183
222
  <Loader2 className="h-4 w-4 animate-spin" />
184
- ) : (
185
- t('common.share')
186
- )}
187
- </Button>
188
- </div>
189
-
190
- <div className="space-y-2">
191
- <div className="font-medium text-sm">
192
- {t('ws-task-boards.share.shared_with')}
193
- </div>
194
- {sharesQuery.isLoading ? (
195
- <div className="rounded-md border border-dashed p-4 text-muted-foreground text-sm">
196
223
  {t('common.loading')}
197
224
  </div>
198
- ) : shares.length === 0 ? (
199
- <div className="rounded-md border border-dashed p-4 text-muted-foreground text-sm">
200
- {t('ws-task-boards.share.empty')}
225
+ ) : (viewableMembersQuery.data?.members ?? []).length === 0 ? (
226
+ <div className="text-muted-foreground text-sm">
227
+ {t('ws-task-boards.share.workspace_members.empty')}
201
228
  </div>
202
229
  ) : (
203
230
  <div className="divide-y rounded-md border">
204
- {shares.map((share) => (
231
+ {viewableMembersQuery.data?.members.map((member) => (
205
232
  <div
206
- key={share.id}
233
+ key={member.user_id}
207
234
  className="flex flex-col gap-3 p-3 sm:flex-row sm:items-center"
208
235
  >
209
236
  <div className="flex min-w-0 flex-1 items-center gap-3">
210
237
  <Avatar className="h-8 w-8">
211
- <AvatarImage
212
- src={share.user?.avatar_url ?? undefined}
213
- />
238
+ <AvatarImage src={member.avatar_url ?? undefined} />
214
239
  <AvatarFallback>
215
- {getInitials(shareDisplayName(share))}
240
+ {getInitials(viewableMemberDisplayName(member))}
216
241
  </AvatarFallback>
217
242
  </Avatar>
218
243
  <div className="min-w-0">
219
244
  <div className="truncate font-medium text-sm">
220
- {shareDisplayName(share)}
245
+ {viewableMemberDisplayName(member)}
221
246
  </div>
222
247
  <div className="truncate text-muted-foreground text-xs">
223
- {share.email || share.user_id}
248
+ {member.email || member.user_id}
224
249
  </div>
225
250
  </div>
226
251
  </div>
227
-
228
- <Badge variant="outline" className="w-fit">
229
- {t('common.guest_access')}
230
- </Badge>
231
-
232
- <Select
233
- value={share.permission}
234
- onValueChange={(value) =>
235
- updateMutation.mutate({
236
- shareId: share.id,
237
- nextPermission:
238
- value as WorkspaceTaskBoardSharePermission,
239
- })
240
- }
241
- >
242
- <SelectTrigger className="w-28">
243
- <SelectValue />
244
- </SelectTrigger>
245
- <SelectContent>
246
- <SelectItem value="view">
247
- {t('ws-task-boards.share.permission.view')}
248
- </SelectItem>
249
- <SelectItem value="edit">
250
- {t('ws-task-boards.share.permission.edit')}
251
- </SelectItem>
252
- </SelectContent>
253
- </Select>
254
-
255
- <Button
256
- type="button"
257
- variant="ghost"
258
- size="icon"
259
- onClick={() => deleteMutation.mutate(share.id)}
260
- disabled={deleteMutation.isPending}
261
- aria-label={t('common.remove')}
262
- >
263
- <Trash2 className="h-4 w-4" />
264
- </Button>
252
+ <div className="flex flex-wrap items-center gap-1.5">
253
+ {member.is_creator && (
254
+ <Badge variant="secondary">
255
+ {t('ws-task-boards.share.workspace_members.creator')}
256
+ </Badge>
257
+ )}
258
+ {member.roles.slice(0, 2).map((role) => (
259
+ <Badge key={role.id} variant="outline">
260
+ {role.name}
261
+ </Badge>
262
+ ))}
263
+ <Badge variant="outline">
264
+ {t('ws-task-boards.share.workspace_members.badge')}
265
+ </Badge>
266
+ </div>
265
267
  </div>
266
268
  ))}
267
269
  </div>
268
270
  )}
269
- </div>
271
+ </ShareSection>
272
+
273
+ <ShareSection
274
+ open={guestsOpen}
275
+ onOpenChange={setGuestsOpen}
276
+ title={t('ws-task-boards.share.guests.title')}
277
+ tooltip={t('ws-task-boards.share.guests.tooltip')}
278
+ icon={<Users className="h-4 w-4 text-muted-foreground" />}
279
+ statusBadge={guestsStatusBadge}
280
+ >
281
+ <div className="space-y-3">
282
+ <div className="grid gap-2 sm:grid-cols-[1fr_8rem_auto]">
283
+ <Input
284
+ type="email"
285
+ value={email}
286
+ onChange={(event) => setEmail(event.target.value)}
287
+ placeholder={t('ws-task-boards.share.email_placeholder')}
288
+ />
289
+ <Combobox
290
+ mode="single"
291
+ options={permissionOptions}
292
+ selected={permission}
293
+ onChange={(value) =>
294
+ setPermission(value as WorkspaceTaskBoardSharePermission)
295
+ }
296
+ placeholder={t('ws-task-boards.share.permission.view')}
297
+ searchPlaceholder={t('common.search_members')}
298
+ className="[&_button]:h-9"
299
+ />
300
+ <Button
301
+ type="button"
302
+ onClick={() => createMutation.mutate()}
303
+ disabled={!canSubmit}
304
+ >
305
+ {createMutation.isPending ? (
306
+ <Loader2 className="h-4 w-4 animate-spin" />
307
+ ) : (
308
+ t('common.share')
309
+ )}
310
+ </Button>
311
+ </div>
312
+
313
+ {sharesQuery.isLoading ? (
314
+ <div className="rounded-md border border-dashed p-4 text-muted-foreground text-sm">
315
+ {t('common.loading')}
316
+ </div>
317
+ ) : shares.length === 0 ? (
318
+ <div className="rounded-md border border-dashed p-4 text-muted-foreground text-sm">
319
+ {t('ws-task-boards.share.empty')}
320
+ </div>
321
+ ) : (
322
+ <div className="divide-y rounded-md border">
323
+ {shares.map((share) => (
324
+ <div
325
+ key={share.id}
326
+ className="flex flex-col gap-3 p-3 sm:flex-row sm:items-center"
327
+ >
328
+ <div className="flex min-w-0 flex-1 items-center gap-3">
329
+ <Avatar className="h-8 w-8">
330
+ <AvatarImage
331
+ src={share.user?.avatar_url ?? undefined}
332
+ />
333
+ <AvatarFallback>
334
+ {getInitials(shareDisplayName(share))}
335
+ </AvatarFallback>
336
+ </Avatar>
337
+ <div className="min-w-0">
338
+ <div className="truncate font-medium text-sm">
339
+ {shareDisplayName(share)}
340
+ </div>
341
+ <div className="truncate text-muted-foreground text-xs">
342
+ {share.email || share.user_id}
343
+ </div>
344
+ </div>
345
+ </div>
346
+
347
+ <Badge variant="outline" className="w-fit">
348
+ {t('common.guest_access')}
349
+ </Badge>
350
+
351
+ <Combobox
352
+ mode="single"
353
+ options={permissionOptions}
354
+ selected={share.permission}
355
+ onChange={(value) =>
356
+ updateMutation.mutate({
357
+ shareId: share.id,
358
+ nextPermission:
359
+ value as WorkspaceTaskBoardSharePermission,
360
+ })
361
+ }
362
+ placeholder={t('ws-task-boards.share.permission.view')}
363
+ searchPlaceholder={t('common.search_members')}
364
+ className="w-28 [&_button]:h-9"
365
+ />
366
+
367
+ <Button
368
+ type="button"
369
+ variant="ghost"
370
+ size="icon"
371
+ onClick={() => deleteMutation.mutate(share.id)}
372
+ disabled={deleteMutation.isPending}
373
+ aria-label={t('common.remove')}
374
+ >
375
+ <Trash2 className="h-4 w-4" />
376
+ </Button>
377
+ </div>
378
+ ))}
379
+ </div>
380
+ )}
381
+ </div>
382
+ </ShareSection>
270
383
  </div>
271
384
 
272
385
  <DialogFooter>