@tuturuuu/ui 0.2.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/package.json +79 -67
  3. package/src/components/ui/__tests__/avatar.test.tsx +8 -5
  4. package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
  5. package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
  6. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
  8. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
  9. package/src/components/ui/chart.test.tsx +29 -0
  10. package/src/components/ui/chart.tsx +12 -3
  11. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +396 -2
  12. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +36 -8
  13. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +14 -0
  14. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +5 -0
  15. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +21 -7
  16. package/src/components/ui/chat/chat-agent-details-utils.test.ts +73 -0
  17. package/src/components/ui/chat/chat-agent-details-utils.tsx +100 -26
  18. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +517 -0
  19. package/src/components/ui/chat/chat-workspace.tsx +31 -1
  20. package/src/components/ui/chat/hooks-messages.test.tsx +45 -1
  21. package/src/components/ui/chat/hooks-messages.ts +1 -1
  22. package/src/components/ui/chat/hooks-realtime.ts +13 -16
  23. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
  24. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
  25. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
  26. package/src/components/ui/custom/common-footer.tsx +16 -1
  27. package/src/components/ui/custom/production-indicator.tsx +1 -1
  28. package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
  29. package/src/components/ui/custom/settings/task-settings.tsx +18 -0
  30. package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
  31. package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
  32. package/src/components/ui/custom/sidebar-context.tsx +61 -61
  33. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
  34. package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
  35. package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
  36. package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
  37. package/src/components/ui/custom/workspace-select.tsx +33 -12
  38. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
  39. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
  40. package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
  41. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
  42. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
  43. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
  44. package/src/components/ui/finance/invoices/hooks.ts +75 -20
  45. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
  46. package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
  47. package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
  48. package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
  49. package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
  50. package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
  51. package/src/components/ui/finance/invoices/utils.test.ts +50 -0
  52. package/src/components/ui/finance/invoices/utils.ts +75 -17
  53. package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
  54. package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
  55. package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
  56. package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
  57. package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
  58. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  59. package/src/components/ui/finance/transactions/form.test.tsx +43 -0
  60. package/src/components/ui/finance/transactions/form.tsx +60 -0
  61. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
  62. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
  63. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
  64. package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
  65. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  66. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
  67. package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
  68. package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
  69. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
  70. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
  71. package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
  72. package/src/components/ui/legacy/meet/page.test.ts +180 -0
  73. package/src/components/ui/legacy/meet/page.tsx +87 -39
  74. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
  75. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
  76. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
  77. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
  78. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
  79. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
  80. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
  81. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
  82. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
  83. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
  84. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
  85. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
  86. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
  87. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
  88. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
  89. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
  90. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
  91. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
  92. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
  93. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
  94. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
  95. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
  96. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
  97. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
  98. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
  99. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
  100. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
  101. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
  102. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
  103. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
  104. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
  105. package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
  106. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
  107. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
  108. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
  109. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
  110. package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
  111. package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
  112. package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
  113. package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
  114. package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
  115. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
  116. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
  117. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
  118. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
  119. package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
  120. package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
  121. package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
  122. package/src/hooks/use-calendar-sync.tsx +22 -277
  123. package/src/hooks/use-calendar.tsx +95 -525
  124. package/src/hooks/use-semantic-task-search.ts +10 -33
  125. package/src/hooks/use-task-actions.ts +43 -117
  126. package/src/hooks/use-user-config.ts +1 -1
  127. package/src/hooks/use-workspace-config.ts +6 -2
  128. package/src/hooks/use-workspace-presence.ts +1 -1
  129. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
@@ -0,0 +1,76 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { render, screen } from '@testing-library/react';
4
+ import type { Workspace } from '@tuturuuu/types';
5
+ import type { ReactNode } from 'react';
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import { SmartCalendar } from './smart-calendar';
8
+
9
+ vi.mock('@tuturuuu/ui/hooks/use-calendar', () => ({
10
+ CalendarProvider: ({ children }: { children: ReactNode }) => (
11
+ <div data-testid="calendar-provider">{children}</div>
12
+ ),
13
+ }));
14
+
15
+ vi.mock('./settings/settings-context', () => ({
16
+ CalendarSettingsProvider: ({ children }: { children: ReactNode }) => (
17
+ <div data-testid="settings-provider">{children}</div>
18
+ ),
19
+ }));
20
+
21
+ vi.mock('./calendar-content', () => ({
22
+ CalendarContent: ({ extras }: { extras?: ReactNode }) => (
23
+ <div data-testid="calendar-content">
24
+ <div data-testid="header-extras">{extras}</div>
25
+ </div>
26
+ ),
27
+ }));
28
+
29
+ vi.mock('../../calendar-app/components/calendar-connections-unified', () => ({
30
+ default: ({ wsId }: { wsId: string }) => (
31
+ <span data-testid="connections-manager">{wsId}</span>
32
+ ),
33
+ }));
34
+
35
+ const baseProps = {
36
+ locale: 'en',
37
+ t: (key: string) => key,
38
+ useQuery: vi.fn(),
39
+ useQueryClient: vi.fn(),
40
+ };
41
+
42
+ describe('SmartCalendar', () => {
43
+ it('renders the compact connections manager before existing header extras', () => {
44
+ render(
45
+ <SmartCalendar
46
+ {...baseProps}
47
+ workspace={{ id: 'workspace-1' } as Workspace}
48
+ extras={<span data-testid="custom-extra">extra</span>}
49
+ />
50
+ );
51
+
52
+ expect(screen.getByTestId('connections-manager').textContent).toBe(
53
+ 'workspace-1'
54
+ );
55
+ expect(
56
+ Array.from(screen.getByTestId('header-extras').children).map((element) =>
57
+ element.getAttribute('data-testid')
58
+ )
59
+ ).toEqual(['connections-manager', 'custom-extra']);
60
+ });
61
+
62
+ it('keeps existing header extras when the manager is disabled', () => {
63
+ render(
64
+ <SmartCalendar
65
+ {...baseProps}
66
+ workspace={{ id: 'workspace-1' } as Workspace}
67
+ disabled
68
+ showConnectionsManager={false}
69
+ extras={<span data-testid="custom-extra">extra</span>}
70
+ />
71
+ );
72
+
73
+ expect(screen.queryByTestId('connections-manager')).toBeNull();
74
+ expect(screen.getByTestId('custom-extra').textContent).toBe('extra');
75
+ });
76
+ });
@@ -6,6 +6,7 @@ import type {
6
6
  } from '@tuturuuu/types';
7
7
  import { CalendarProvider } from '@tuturuuu/ui/hooks/use-calendar';
8
8
  import type { CalendarView } from '../../../../hooks/use-view-transition';
9
+ import CalendarConnectionsUnified from '../../calendar-app/components/calendar-connections-unified';
9
10
  import { CalendarContent } from './calendar-content';
10
11
  import {
11
12
  type CalendarSettings,
@@ -26,6 +27,7 @@ export const SmartCalendar = ({
26
27
  overlay,
27
28
  initialSettings,
28
29
  onSaveSettings,
30
+ showConnectionsManager = true,
29
31
  }: {
30
32
  t: any;
31
33
  locale: string;
@@ -46,12 +48,22 @@ export const SmartCalendar = ({
46
48
  overlay?: React.ReactNode;
47
49
  initialSettings?: Partial<CalendarSettings>;
48
50
  onSaveSettings?: (settings: CalendarSettings) => Promise<void>;
51
+ showConnectionsManager?: boolean;
49
52
  }) => {
50
53
  const handleSaveSettings = async (newSettings: CalendarSettings) => {
51
54
  if (onSaveSettings) {
52
55
  await onSaveSettings(newSettings);
53
56
  }
54
57
  };
58
+ const headerExtras =
59
+ showConnectionsManager && workspace?.id ? (
60
+ <>
61
+ <CalendarConnectionsUnified wsId={workspace.id} />
62
+ {extras}
63
+ </>
64
+ ) : (
65
+ extras
66
+ );
55
67
 
56
68
  return (
57
69
  <CalendarProvider
@@ -73,7 +85,7 @@ export const SmartCalendar = ({
73
85
  enableHeader={enableHeader}
74
86
  experimentalGoogleToken={experimentalGoogleToken}
75
87
  externalState={externalState}
76
- extras={extras}
88
+ extras={headerExtras}
77
89
  overlay={overlay}
78
90
  />
79
91
  </CalendarSettingsProvider>
@@ -0,0 +1,180 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getMeetTogetherPlansData } from './page';
3
+
4
+ const mocks = vi.hoisted(() => ({
5
+ createAdminClient: vi.fn(),
6
+ createClient: vi.fn(),
7
+ }));
8
+
9
+ vi.mock('@tuturuuu/supabase/next/server', () => ({
10
+ createAdminClient: (...args: Parameters<typeof mocks.createAdminClient>) =>
11
+ mocks.createAdminClient(...args),
12
+ createClient: (...args: Parameters<typeof mocks.createClient>) =>
13
+ mocks.createClient(...args),
14
+ }));
15
+
16
+ vi.mock('next-intl/server', () => ({
17
+ getLocale: vi.fn().mockResolvedValue('en'),
18
+ getTranslations: vi.fn().mockResolvedValue((key: string) => key),
19
+ }));
20
+
21
+ type QueryCall = {
22
+ column?: string;
23
+ table: string;
24
+ type: 'select' | 'eq' | 'neq' | 'order' | 'in';
25
+ value?: unknown;
26
+ };
27
+
28
+ function createBuilder(table: string, data: unknown[], calls: QueryCall[]) {
29
+ const builder = Object.assign(Promise.resolve({ data, error: null }), {
30
+ eq: vi.fn((column: string, value: unknown) => {
31
+ calls.push({ column, table, type: 'eq', value });
32
+ return builder;
33
+ }),
34
+ in: vi.fn((column: string, value: unknown) => {
35
+ calls.push({ column, table, type: 'in', value });
36
+ return builder;
37
+ }),
38
+ neq: vi.fn((column: string, value: unknown) => {
39
+ calls.push({ column, table, type: 'neq', value });
40
+ return builder;
41
+ }),
42
+ order: vi.fn((column: string, value: unknown) => {
43
+ calls.push({ column, table, type: 'order', value });
44
+ return builder;
45
+ }),
46
+ select: vi.fn((column: string) => {
47
+ calls.push({ column, table, type: 'select' });
48
+ return builder;
49
+ }),
50
+ });
51
+
52
+ return builder;
53
+ }
54
+
55
+ describe('MeetTogether plans data', () => {
56
+ beforeEach(() => {
57
+ vi.clearAllMocks();
58
+ mocks.createClient.mockResolvedValue({
59
+ auth: {
60
+ getUser: vi.fn().mockResolvedValue({
61
+ data: { user: { id: 'user-1' } },
62
+ }),
63
+ },
64
+ });
65
+ });
66
+
67
+ it('consolidates personal workspace plans with previously interacted plans', async () => {
68
+ const calls: QueryCall[] = [];
69
+ const personalPlan = {
70
+ created_at: '2026-01-01T00:00:00.000Z',
71
+ creator_id: 'user-1',
72
+ id: 'personal-created',
73
+ ws_id: 'personal-ws',
74
+ };
75
+ const interactedPlan = {
76
+ created_at: '2026-01-03T00:00:00.000Z',
77
+ creator_id: 'other-user',
78
+ id: 'team-interacted',
79
+ ws_id: 'team-ws',
80
+ };
81
+ const participant = {
82
+ display_name: 'User',
83
+ is_guest: false,
84
+ plan_id: 'team-interacted',
85
+ timeblock_count: 1,
86
+ user_id: 'user-1',
87
+ };
88
+ const builders = [
89
+ createBuilder('meet_together_plans', [personalPlan], calls),
90
+ createBuilder(
91
+ 'meet_together_user_timeblocks',
92
+ [interactedPlan, personalPlan],
93
+ calls
94
+ ),
95
+ createBuilder('meet_together_users', [participant], calls),
96
+ ];
97
+
98
+ mocks.createAdminClient.mockResolvedValue({
99
+ from: vi.fn(() => builders.shift()),
100
+ });
101
+
102
+ const result = await getMeetTogetherPlansData({
103
+ scope: 'personal-consolidated',
104
+ wsId: 'personal-ws',
105
+ });
106
+
107
+ expect(result.data.map((plan) => plan.id)).toEqual([
108
+ 'team-interacted',
109
+ 'personal-created',
110
+ ]);
111
+ expect(result.totalCount).toBe(2);
112
+ expect(result.data[0]?.participants).toEqual([participant]);
113
+ expect(calls).toEqual(
114
+ expect.arrayContaining([
115
+ {
116
+ column: 'ws_id',
117
+ table: 'meet_together_plans',
118
+ type: 'eq',
119
+ value: 'personal-ws',
120
+ },
121
+ {
122
+ column: 'user_id',
123
+ table: 'meet_together_user_timeblocks',
124
+ type: 'eq',
125
+ value: 'user-1',
126
+ },
127
+ ])
128
+ );
129
+ expect(calls).not.toEqual(
130
+ expect.arrayContaining([
131
+ expect.objectContaining({
132
+ column: 'meet_together_plans.ws_id',
133
+ table: 'meet_together_user_timeblocks',
134
+ type: 'eq',
135
+ }),
136
+ ])
137
+ );
138
+ });
139
+
140
+ it('keeps team workspace plans scoped to the active workspace', async () => {
141
+ const calls: QueryCall[] = [];
142
+ const builders = [
143
+ createBuilder('meet_together_plans', [], calls),
144
+ createBuilder('meet_together_user_timeblocks', [], calls),
145
+ createBuilder('meet_together_users', [], calls),
146
+ ];
147
+
148
+ mocks.createAdminClient.mockResolvedValue({
149
+ from: vi.fn(() => builders.shift()),
150
+ });
151
+
152
+ await getMeetTogetherPlansData({
153
+ scope: 'workspace',
154
+ wsId: 'team-ws',
155
+ });
156
+
157
+ expect(calls).toEqual(
158
+ expect.arrayContaining([
159
+ {
160
+ column: 'ws_id',
161
+ table: 'meet_together_plans',
162
+ type: 'eq',
163
+ value: 'team-ws',
164
+ },
165
+ {
166
+ column: 'meet_together_plans.ws_id',
167
+ table: 'meet_together_user_timeblocks',
168
+ type: 'eq',
169
+ value: 'team-ws',
170
+ },
171
+ {
172
+ column: 'meet_together_plans.creator_id',
173
+ table: 'meet_together_user_timeblocks',
174
+ type: 'neq',
175
+ value: 'user-1',
176
+ },
177
+ ])
178
+ );
179
+ });
180
+ });
@@ -27,8 +27,18 @@ export interface MeetTogetherPlanWithParticipants extends MeetTogetherPlan {
27
27
  }>;
28
28
  }
29
29
 
30
+ type MeetTogetherPlanRow = Record<string, unknown> & {
31
+ created_at?: string | null;
32
+ creator_id?: string | null;
33
+ description?: string | null;
34
+ id?: string | null;
35
+ name?: string | null;
36
+ ws_id?: string | null;
37
+ };
38
+
30
39
  // Server component props type
31
40
  interface MeetTogetherPageProps {
41
+ scope?: MeetTogetherPlansScope;
32
42
  wsId?: string;
33
43
  path?: string;
34
44
  searchParams?: Promise<{
@@ -38,7 +48,10 @@ interface MeetTogetherPageProps {
38
48
  }>;
39
49
  }
40
50
 
51
+ export type MeetTogetherPlansScope = 'workspace' | 'personal-consolidated';
52
+
41
53
  export async function MeetTogetherPage({
54
+ scope = 'workspace',
42
55
  wsId,
43
56
  path,
44
57
  searchParams,
@@ -55,7 +68,7 @@ export async function MeetTogetherPage({
55
68
  data: plans,
56
69
  user,
57
70
  totalCount,
58
- } = await getData({ wsId, page, pageSize });
71
+ } = await getMeetTogetherPlansData({ scope, wsId, page, pageSize });
59
72
  const totalPages = Math.ceil(totalCount / pageSize);
60
73
 
61
74
  return (
@@ -222,11 +235,13 @@ export async function MeetTogetherPage({
222
235
  );
223
236
  }
224
237
 
225
- async function getData({
238
+ export async function getMeetTogetherPlansData({
239
+ scope = 'workspace',
226
240
  wsId,
227
241
  page = 1,
228
242
  pageSize = 9,
229
243
  }: {
244
+ scope?: MeetTogetherPlansScope;
230
245
  wsId?: string;
231
246
  page?: number;
232
247
  pageSize?: number;
@@ -241,47 +256,80 @@ async function getData({
241
256
 
242
257
  const sbAdmin = await createAdminClient();
243
258
 
244
- const createdPlansQuery = sbAdmin
245
- .from('meet_together_plans')
246
- .select('*')
247
- .eq('creator_id', user.id)
248
- .order('created_at', { ascending: false });
249
-
250
- const joinedPlansQuery = sbAdmin
251
- .from('meet_together_user_timeblocks')
252
- .select('...meet_together_plans!inner(*)')
253
- .eq('user_id', user.id)
254
- .neq('meet_together_plans.creator_id', user.id)
255
- .order('created_at', {
256
- ascending: false,
257
- referencedTable: 'meet_together_plans',
259
+ const planMap = new Map<string, MeetTogetherPlan>();
260
+ const addPlans = (plans: MeetTogetherPlanRow[] | null | undefined) => {
261
+ plans?.forEach((plan) => {
262
+ const planId = plan.id ?? undefined;
263
+
264
+ if (planId && !planMap.has(planId)) {
265
+ planMap.set(planId, {
266
+ ...plan,
267
+ agenda_content: plan.agenda_content ?? undefined,
268
+ created_at: plan.created_at ?? undefined,
269
+ creator_id: plan.creator_id ?? undefined,
270
+ description: plan.description ?? undefined,
271
+ id: planId,
272
+ name: plan.name ?? undefined,
273
+ ws_id: plan.ws_id ?? undefined,
274
+ } as MeetTogetherPlan);
275
+ }
258
276
  });
277
+ };
259
278
 
260
- if (wsId) {
261
- createdPlansQuery.eq('ws_id', wsId);
262
- joinedPlansQuery.eq('meet_together_plans.ws_id', wsId);
263
- }
264
-
265
- const [createdPlansResult, joinedPlansResult] = await Promise.all([
266
- createdPlansQuery,
267
- joinedPlansQuery,
268
- ]);
279
+ if (scope === 'personal-consolidated' && wsId) {
280
+ const personalPlansQuery = sbAdmin
281
+ .from('meet_together_plans')
282
+ .select('*')
283
+ .eq('creator_id', user.id)
284
+ .eq('ws_id', wsId)
285
+ .order('created_at', { ascending: false });
286
+
287
+ const interactedPlansQuery = sbAdmin
288
+ .from('meet_together_user_timeblocks')
289
+ .select('...meet_together_plans!inner(*)')
290
+ .eq('user_id', user.id)
291
+ .order('created_at', {
292
+ ascending: false,
293
+ referencedTable: 'meet_together_plans',
294
+ });
295
+
296
+ const [personalPlansResult, interactedPlansResult] = await Promise.all([
297
+ personalPlansQuery,
298
+ interactedPlansQuery,
299
+ ]);
300
+
301
+ addPlans(personalPlansResult.data);
302
+ addPlans(interactedPlansResult.data);
303
+ } else {
304
+ const createdPlansQuery = sbAdmin
305
+ .from('meet_together_plans')
306
+ .select('*')
307
+ .eq('creator_id', user.id)
308
+ .order('created_at', { ascending: false });
309
+
310
+ const joinedPlansQuery = sbAdmin
311
+ .from('meet_together_user_timeblocks')
312
+ .select('...meet_together_plans!inner(*)')
313
+ .eq('user_id', user.id)
314
+ .neq('meet_together_plans.creator_id', user.id)
315
+ .order('created_at', {
316
+ ascending: false,
317
+ referencedTable: 'meet_together_plans',
318
+ });
319
+
320
+ if (wsId) {
321
+ createdPlansQuery.eq('ws_id', wsId);
322
+ joinedPlansQuery.eq('meet_together_plans.ws_id', wsId);
323
+ }
269
324
 
270
- const { data: createdPlans } = createdPlansResult;
271
- const { data: joinedPlans } = joinedPlansResult;
325
+ const [createdPlansResult, joinedPlansResult] = await Promise.all([
326
+ createdPlansQuery,
327
+ joinedPlansQuery,
328
+ ]);
272
329
 
273
- // Combine and deduplicate
274
- const planMap = new Map();
275
- createdPlans?.forEach((plan) => {
276
- if (!planMap.has(plan.id)) {
277
- planMap.set(plan.id, plan);
278
- }
279
- });
280
- joinedPlans?.forEach((plan) => {
281
- if (!planMap.has(plan.id)) {
282
- planMap.set(plan.id, plan);
283
- }
284
- });
330
+ addPlans(createdPlansResult.data);
331
+ addPlans(joinedPlansResult.data);
332
+ }
285
333
 
286
334
  const allPlans = Array.from(planMap.values()).sort(
287
335
  (a, b) =>
@@ -40,8 +40,13 @@ import {
40
40
  type ListPaginationState,
41
41
  useProgressiveLoader,
42
42
  } from '../../shared/progressive-loader-context';
43
+ import { getListTextColorClass } from '../../utils/taskColorUtils';
43
44
  import { normalizeBoardText } from './board-text-utils';
44
45
  import type { DragPreviewPosition } from './kanban/dnd/use-kanban-dnd';
46
+ import {
47
+ isClosedTaskListColumnCollapsed,
48
+ isKanbanColumnCollapsed,
49
+ } from './kanban/kanban-column-collapse';
45
50
  import { ListActions } from './list-actions';
46
51
  import { statusIcons } from './status-section';
47
52
  import type { TaskFilters } from './task-filter';
@@ -168,6 +173,7 @@ interface BoardColumnProps {
168
173
  workspaceId?: string;
169
174
  wsId: string;
170
175
  onExternalTasksCollapsedChange?: (collapsed: boolean) => void;
176
+ onTaskListCollapsedChange?: (listId: string, collapsed: boolean) => void;
171
177
  }
172
178
 
173
179
  export function BoardColumn({
@@ -193,6 +199,7 @@ export function BoardColumn({
193
199
  workspaceId,
194
200
  wsId,
195
201
  onExternalTasksCollapsedChange,
202
+ onTaskListCollapsedChange,
196
203
  }: BoardColumnProps) {
197
204
  const t = useTranslations('common');
198
205
  const tTasks = useTranslations('ws-tasks');
@@ -211,6 +218,8 @@ export function BoardColumn({
211
218
  const [externalSortBy, setExternalSortBy] = useState<ExternalTaskSortBy>(
212
219
  DEFAULT_EXTERNAL_TASK_SORT_BY
213
220
  );
221
+ const isClosedCollapsed = isClosedTaskListColumnCollapsed(column);
222
+ const isColumnCollapsed = isKanbanColumnCollapsed(column);
214
223
  const hasActiveFilters =
215
224
  !!filters &&
216
225
  (filters.labels.length > 0 ||
@@ -291,7 +300,7 @@ export function BoardColumn({
291
300
  useEffect(() => {
292
301
  if (
293
302
  !isExternalStaging ||
294
- isExternalCollapsed ||
303
+ isColumnCollapsed ||
295
304
  !listState ||
296
305
  listState.isInitialLoad ||
297
306
  listState.isLoading ||
@@ -303,7 +312,7 @@ export function BoardColumn({
303
312
  loadColumnPage(0);
304
313
  }, [
305
314
  externalOptionsSignature,
306
- isExternalCollapsed,
315
+ isColumnCollapsed,
307
316
  isExternalStaging,
308
317
  listState,
309
318
  listState?.isInitialLoad,
@@ -315,7 +324,7 @@ export function BoardColumn({
315
324
  // cache was cleared, refetch page 0 for this list so cards reappear.
316
325
  useEffect(() => {
317
326
  if (
318
- isExternalCollapsed ||
327
+ isColumnCollapsed ||
319
328
  !listState ||
320
329
  listState.isLoading ||
321
330
  hasActiveFilters
@@ -334,7 +343,7 @@ export function BoardColumn({
334
343
  recoveryRequestedRef.current = false;
335
344
  }, [
336
345
  hasActiveFilters,
337
- isExternalCollapsed,
346
+ isColumnCollapsed,
338
347
  listState,
339
348
  listState?.isLoading,
340
349
  listState?.totalCount,
@@ -481,22 +490,45 @@ export function BoardColumn({
481
490
  });
482
491
  };
483
492
 
484
- if (isExternalCollapsed) {
493
+ if (isColumnCollapsed) {
494
+ const collapsedListName = translateListName(column.name);
495
+ const expandLabel = isExternalCollapsed
496
+ ? tTasks('expand_external_tasks')
497
+ : tTasks('expand_task_list', { name: collapsedListName });
498
+ const collapsedTextColor = isExternalCollapsed
499
+ ? 'text-dynamic-cyan'
500
+ : getListTextColorClass(column.color as SupportedColor);
501
+
485
502
  return (
486
503
  <Card
487
504
  ref={composedRef}
488
505
  style={style}
489
506
  className={cn(
490
- 'group flex h-full w-14 shrink-0 snap-start flex-col items-center rounded-xl border border-dynamic-cyan/45 border-dashed bg-dynamic-cyan/[0.035] transition-all duration-200',
491
- 'touch-none select-none overflow-hidden hover:shadow-md'
507
+ 'group flex h-full w-14 shrink-0 snap-start flex-col items-center rounded-xl border border-dashed transition-all duration-200',
508
+ 'touch-none select-none overflow-hidden hover:shadow-md',
509
+ isExternalCollapsed
510
+ ? 'border-dynamic-cyan/45 bg-dynamic-cyan/[0.035]'
511
+ : colorClass
492
512
  )}
493
513
  >
494
514
  <button
495
515
  type="button"
496
- className="flex h-full w-full flex-col items-center gap-3 rounded-xl px-1 py-3 text-dynamic-cyan transition-colors hover:bg-dynamic-cyan/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-dynamic-cyan/40"
497
- title={tTasks('expand_external_tasks')}
498
- aria-label={tTasks('expand_external_tasks')}
499
- onClick={() => onExternalTasksCollapsedChange?.(false)}
516
+ className={cn(
517
+ 'flex h-full w-full flex-col items-center gap-3 rounded-xl px-1 py-3 transition-colors focus-visible:outline-none focus-visible:ring-2',
518
+ isExternalCollapsed
519
+ ? 'text-dynamic-cyan hover:bg-dynamic-cyan/10 focus-visible:ring-dynamic-cyan/40'
520
+ : `${collapsedTextColor} hover:bg-muted/40 focus-visible:ring-primary/40`
521
+ )}
522
+ title={expandLabel}
523
+ aria-label={expandLabel}
524
+ onClick={() => {
525
+ if (isExternalCollapsed) {
526
+ onExternalTasksCollapsedChange?.(false);
527
+ return;
528
+ }
529
+
530
+ onTaskListCollapsedChange?.(column.id, false);
531
+ }}
500
532
  >
501
533
  <ChevronRight className="h-4 w-4 shrink-0" />
502
534
  <Badge
@@ -509,7 +541,7 @@ export function BoardColumn({
509
541
  className="max-h-48 truncate font-medium text-[11px]"
510
542
  style={{ writingMode: 'vertical-rl' }}
511
543
  >
512
- {translateListName(column.name)}
544
+ {collapsedListName}
513
545
  </span>
514
546
  </button>
515
547
  </Card>
@@ -688,19 +720,41 @@ export function BoardColumn({
688
720
  </Button>
689
721
  </>
690
722
  ) : (
691
- <ListActions
692
- listId={column.id}
693
- listName={column.name}
694
- listStatus={column.status}
695
- listColor={column.color as SupportedColor}
696
- tasks={tasks}
697
- boardId={boardId}
698
- wsId={wsId}
699
- onUpdate={handleUpdate}
700
- onSelectAll={handleSelectAll}
701
- isEditOpen={isEditOpen}
702
- onEditOpenChange={setIsEditOpen}
703
- />
723
+ <>
724
+ {isClosedCollapsed || column.status === 'closed' ? (
725
+ <Button
726
+ type="button"
727
+ variant="ghost"
728
+ size="xs"
729
+ className={cn(
730
+ 'h-7 w-7 p-0 hover:bg-muted/40',
731
+ getListTextColorClass(column.color as SupportedColor)
732
+ )}
733
+ title={tTasks('collapse_task_list', {
734
+ name: translateListName(column.name),
735
+ })}
736
+ aria-label={tTasks('collapse_task_list', {
737
+ name: translateListName(column.name),
738
+ })}
739
+ onClick={() => onTaskListCollapsedChange?.(column.id, true)}
740
+ >
741
+ <ChevronLeft className="h-3.5 w-3.5" />
742
+ </Button>
743
+ ) : null}
744
+ <ListActions
745
+ listId={column.id}
746
+ listName={column.name}
747
+ listStatus={column.status}
748
+ listColor={column.color as SupportedColor}
749
+ tasks={tasks}
750
+ boardId={boardId}
751
+ wsId={wsId}
752
+ onUpdate={handleUpdate}
753
+ onSelectAll={handleSelectAll}
754
+ isEditOpen={isEditOpen}
755
+ onEditOpenChange={setIsEditOpen}
756
+ />
757
+ </>
704
758
  )}
705
759
  </div>
706
760
  </div>