@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
@@ -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>
@@ -11,6 +11,8 @@ import {
11
11
  Filter,
12
12
  GripVertical,
13
13
  Loader2,
14
+ Pin,
15
+ PinOff,
14
16
  RotateCcw,
15
17
  } from '@tuturuuu/icons';
16
18
  import type { ExternalTaskSortBy } from '@tuturuuu/internal-api/tasks';
@@ -174,6 +176,9 @@ interface BoardColumnProps {
174
176
  wsId: string;
175
177
  onExternalTasksCollapsedChange?: (collapsed: boolean) => void;
176
178
  onTaskListCollapsedChange?: (listId: string, collapsed: boolean) => void;
179
+ specialPinned?: boolean;
180
+ onSpecialPinnedChange?: (pinned: boolean) => void;
181
+ readOnly?: boolean;
177
182
  }
178
183
 
179
184
  export function BoardColumn({
@@ -200,6 +205,9 @@ export function BoardColumn({
200
205
  wsId,
201
206
  onExternalTasksCollapsedChange,
202
207
  onTaskListCollapsedChange,
208
+ specialPinned = false,
209
+ onSpecialPinnedChange,
210
+ readOnly = false,
203
211
  }: BoardColumnProps) {
204
212
  const t = useTranslations('common');
205
213
  const tTasks = useTranslations('ws-tasks');
@@ -210,7 +218,9 @@ export function BoardColumn({
210
218
  const isExternalCollapsed =
211
219
  isExternalStaging && column.is_external_collapsed === true;
212
220
  const listState = pagination[column.id];
213
- const isInitialLoad = !listState || listState.isInitialLoad;
221
+ const isInitialLoad = readOnly
222
+ ? false
223
+ : !listState || listState.isInitialLoad;
214
224
  const [externalIncludeDocuments, setExternalIncludeDocuments] =
215
225
  useState(false);
216
226
  const [externalIncludeDoneClosed, setExternalIncludeDoneClosed] =
@@ -376,7 +386,7 @@ export function BoardColumn({
376
386
  isDragging,
377
387
  } = useSortable({
378
388
  id: column.id,
379
- disabled: isExternalStaging,
389
+ disabled: readOnly || isExternalStaging,
380
390
  data: {
381
391
  type: 'Column',
382
392
  column: {
@@ -446,6 +456,9 @@ export function BoardColumn({
446
456
  : visibleTasks.length;
447
457
  const externalFilterCount =
448
458
  (externalIncludeDocuments ? 1 : 0) + (externalIncludeDoneClosed ? 1 : 0);
459
+ const pinListLabel = specialPinned
460
+ ? tTasks('unpin_task_list', { name: translateListName(column.name) })
461
+ : tTasks('pin_task_list', { name: translateListName(column.name) });
449
462
 
450
463
  // Memoize drag handle for performance
451
464
  const DragHandle = useMemo(
@@ -501,6 +514,8 @@ export function BoardColumn({
501
514
  <Card
502
515
  ref={composedRef}
503
516
  style={style}
517
+ data-kanban-column-id={column.id}
518
+ data-kanban-real-column={isExternalStaging ? undefined : 'true'}
504
519
  className={cn(
505
520
  'group flex h-full w-14 shrink-0 snap-start flex-col items-center rounded-xl border border-dashed transition-all duration-200',
506
521
  'touch-none select-none overflow-hidden hover:shadow-md',
@@ -550,6 +565,8 @@ export function BoardColumn({
550
565
  <Card
551
566
  ref={composedRef}
552
567
  style={style}
568
+ data-kanban-column-id={column.id}
569
+ data-kanban-real-column={isExternalStaging ? undefined : 'true'}
553
570
  className={cn(
554
571
  'group flex h-full w-[var(--kanban-column-width)] shrink-0 snap-start flex-col rounded-xl transition-all duration-200 last:snap-end',
555
572
  'touch-none select-none',
@@ -566,7 +583,7 @@ export function BoardColumn({
566
583
  )}
567
584
  >
568
585
  <div className="flex items-center gap-2 rounded-t-xl border-b p-3">
569
- {!isExternalStaging && DragHandle}
586
+ {!readOnly && !isExternalStaging && DragHandle}
570
587
  <div className="flex flex-1 items-center gap-2">
571
588
  <span className="text-sm">{statusIcon}</span>
572
589
  <h3
@@ -577,9 +594,9 @@ export function BoardColumn({
577
594
  : 'cursor-pointer hover:underline'
578
595
  )}
579
596
  onClick={() => {
580
- if (!isExternalStaging) setIsEditOpen(true);
597
+ if (!readOnly && !isExternalStaging) setIsEditOpen(true);
581
598
  }}
582
- title={isExternalStaging ? undefined : t('edit_list')}
599
+ title={readOnly || isExternalStaging ? undefined : t('edit_list')}
583
600
  >
584
601
  {translateListName(column.name)}
585
602
  </h3>
@@ -705,53 +722,104 @@ export function BoardColumn({
705
722
  </DropdownMenuRadioGroup>
706
723
  </DropdownMenuContent>
707
724
  </DropdownMenu>
708
- <Button
709
- type="button"
710
- variant="ghost"
711
- size="xs"
712
- className="h-7 w-7 p-0 text-dynamic-cyan hover:bg-dynamic-cyan/10"
713
- title={tTasks('collapse_external_tasks')}
714
- aria-label={tTasks('collapse_external_tasks')}
715
- onClick={() => onExternalTasksCollapsedChange?.(true)}
716
- >
717
- <ChevronLeft className="h-3.5 w-3.5" />
718
- </Button>
719
- </>
720
- ) : (
721
- <>
722
- {isClosedCollapsed || column.status === 'closed' ? (
725
+ {onSpecialPinnedChange ? (
723
726
  <Button
724
727
  type="button"
725
728
  variant="ghost"
726
729
  size="xs"
727
730
  className={cn(
728
- 'h-7 w-7 p-0 hover:bg-muted/40',
729
- getListTextColorClass(column.color as SupportedColor)
731
+ 'h-7 w-7 p-0 text-dynamic-cyan hover:bg-dynamic-cyan/10',
732
+ specialPinned && 'bg-dynamic-cyan/10'
730
733
  )}
731
- title={tTasks('collapse_task_list', {
732
- name: translateListName(column.name),
733
- })}
734
- aria-label={tTasks('collapse_task_list', {
735
- name: translateListName(column.name),
736
- })}
737
- onClick={() => onTaskListCollapsedChange?.(column.id, true)}
734
+ title={pinListLabel}
735
+ aria-label={pinListLabel}
736
+ onClick={() => onSpecialPinnedChange(!specialPinned)}
737
+ >
738
+ {specialPinned ? (
739
+ <PinOff className="h-3.5 w-3.5" />
740
+ ) : (
741
+ <Pin className="h-3.5 w-3.5" />
742
+ )}
743
+ </Button>
744
+ ) : null}
745
+ {!specialPinned && (
746
+ <Button
747
+ type="button"
748
+ variant="ghost"
749
+ size="xs"
750
+ className="h-7 w-7 p-0 text-dynamic-cyan hover:bg-dynamic-cyan/10"
751
+ title={tTasks('collapse_external_tasks')}
752
+ aria-label={tTasks('collapse_external_tasks')}
753
+ onClick={() => onExternalTasksCollapsedChange?.(true)}
738
754
  >
739
755
  <ChevronLeft className="h-3.5 w-3.5" />
740
756
  </Button>
757
+ )}
758
+ </>
759
+ ) : (
760
+ <>
761
+ {isClosedCollapsed || column.status === 'closed' ? (
762
+ <>
763
+ {onSpecialPinnedChange ? (
764
+ <Button
765
+ type="button"
766
+ variant="ghost"
767
+ size="xs"
768
+ className={cn(
769
+ 'h-7 w-7 p-0 hover:bg-muted/40',
770
+ getListTextColorClass(column.color as SupportedColor),
771
+ specialPinned && 'bg-muted/40'
772
+ )}
773
+ title={pinListLabel}
774
+ aria-label={pinListLabel}
775
+ onClick={() => onSpecialPinnedChange(!specialPinned)}
776
+ >
777
+ {specialPinned ? (
778
+ <PinOff className="h-3.5 w-3.5" />
779
+ ) : (
780
+ <Pin className="h-3.5 w-3.5" />
781
+ )}
782
+ </Button>
783
+ ) : null}
784
+ {!specialPinned && (
785
+ <Button
786
+ type="button"
787
+ variant="ghost"
788
+ size="xs"
789
+ className={cn(
790
+ 'h-7 w-7 p-0 hover:bg-muted/40',
791
+ getListTextColorClass(column.color as SupportedColor)
792
+ )}
793
+ title={tTasks('collapse_task_list', {
794
+ name: translateListName(column.name),
795
+ })}
796
+ aria-label={tTasks('collapse_task_list', {
797
+ name: translateListName(column.name),
798
+ })}
799
+ onClick={() =>
800
+ onTaskListCollapsedChange?.(column.id, true)
801
+ }
802
+ >
803
+ <ChevronLeft className="h-3.5 w-3.5" />
804
+ </Button>
805
+ )}
806
+ </>
741
807
  ) : null}
742
- <ListActions
743
- listId={column.id}
744
- listName={column.name}
745
- listStatus={column.status}
746
- listColor={column.color as SupportedColor}
747
- tasks={tasks}
748
- boardId={boardId}
749
- wsId={wsId}
750
- onUpdate={handleUpdate}
751
- onSelectAll={handleSelectAll}
752
- isEditOpen={isEditOpen}
753
- onEditOpenChange={setIsEditOpen}
754
- />
808
+ {!readOnly && (
809
+ <ListActions
810
+ listId={column.id}
811
+ listName={column.name}
812
+ listStatus={column.status}
813
+ listColor={column.color as SupportedColor}
814
+ tasks={tasks}
815
+ boardId={boardId}
816
+ wsId={wsId}
817
+ onUpdate={handleUpdate}
818
+ onSelectAll={handleSelectAll}
819
+ isEditOpen={isEditOpen}
820
+ onEditOpenChange={setIsEditOpen}
821
+ />
822
+ )}
755
823
  </>
756
824
  )}
757
825
  </div>
@@ -783,10 +851,11 @@ export function BoardColumn({
783
851
  onLoadMore={handleLoadMore}
784
852
  hasMore={listState?.hasMore ?? false}
785
853
  isLoadingMore={listState?.isLoading ?? false}
854
+ readOnly={readOnly}
786
855
  />
787
856
  )}
788
857
 
789
- {!isExternalStaging && (
858
+ {!readOnly && !isExternalStaging && (
790
859
  <div className="rounded-b-xl border-t p-3 backdrop-blur-sm">
791
860
  <Button
792
861
  variant="ghost"