@tuturuuu/ui 0.1.0 → 0.3.1

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 (128) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/package.json +82 -70
  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-external-thread-panel.test.tsx +43 -13
  12. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +138 -74
  13. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
  14. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +60 -1
  15. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +13 -5
  16. package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
  17. package/src/components/ui/chat/chat-sidebar-panel.tsx +13 -3
  18. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
  19. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
  20. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
  21. package/src/components/ui/custom/common-footer.tsx +16 -1
  22. package/src/components/ui/custom/production-indicator.tsx +1 -1
  23. package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
  24. package/src/components/ui/custom/settings/task-settings.tsx +18 -0
  25. package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
  26. package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
  27. package/src/components/ui/custom/sidebar-context.tsx +61 -61
  28. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
  29. package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
  30. package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
  31. package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
  32. package/src/components/ui/custom/workspace-select.tsx +33 -12
  33. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
  34. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
  35. package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
  36. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
  37. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
  38. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
  39. package/src/components/ui/finance/invoices/hooks.ts +75 -20
  40. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
  41. package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
  42. package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
  43. package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
  44. package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
  45. package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
  46. package/src/components/ui/finance/invoices/utils.test.ts +50 -0
  47. package/src/components/ui/finance/invoices/utils.ts +75 -17
  48. package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
  49. package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
  50. package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
  51. package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
  52. package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
  53. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  54. package/src/components/ui/finance/transactions/form.test.tsx +43 -0
  55. package/src/components/ui/finance/transactions/form.tsx +60 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
  57. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
  58. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
  59. package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
  60. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  61. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
  62. package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
  63. package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
  64. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
  65. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
  66. package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
  67. package/src/components/ui/legacy/meet/page.test.ts +180 -0
  68. package/src/components/ui/legacy/meet/page.tsx +87 -39
  69. package/src/components/ui/legacy/meet/planId/page.tsx +10 -4
  70. package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +203 -6
  71. package/src/components/ui/text-editor/task-mention-chip.tsx +29 -7
  72. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
  73. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
  74. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
  75. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
  76. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
  77. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
  78. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
  79. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
  80. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
  81. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
  82. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
  83. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
  84. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
  85. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
  86. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
  87. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
  88. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
  89. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
  90. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
  91. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
  92. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
  93. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
  94. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
  95. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
  96. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
  97. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
  98. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
  99. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
  100. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
  101. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
  102. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
  103. package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
  104. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
  105. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
  106. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
  107. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
  108. package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
  109. package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
  110. package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
  111. package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
  112. package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
  113. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-realtime-sync.test.tsx +37 -9
  114. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
  115. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +89 -70
  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-task-actions.ts +43 -117
  125. package/src/hooks/use-user-config.ts +1 -1
  126. package/src/hooks/use-workspace-config.ts +6 -2
  127. package/src/hooks/use-workspace-presence.ts +1 -1
  128. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
@@ -1,6 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import { CreateDialogFeatureSummary } from '@tuturuuu/ui/finance/shared/create-dialog-feature-summary';
4
+ import {
5
+ type FinancePermissionRequestUser,
6
+ FinancePermissionWarningContent,
7
+ } from '@tuturuuu/ui/finance/shared/finance-permission-warning-dialog';
4
8
  import { TransactionForm } from '@tuturuuu/ui/finance/transactions/form';
5
9
 
6
10
  interface TransactionsCreateSummaryProps {
@@ -12,6 +16,7 @@ interface TransactionsCreateSummaryProps {
12
16
  createTitle: string;
13
17
  defaultOpen?: boolean;
14
18
  description: string;
19
+ permissionRequestUser?: FinancePermissionRequestUser | null;
15
20
  pluralTitle: string;
16
21
  singularTitle: string;
17
22
  wsId: string;
@@ -26,6 +31,7 @@ export function TransactionsCreateSummary({
26
31
  createTitle,
27
32
  defaultOpen = false,
28
33
  description,
34
+ permissionRequestUser,
29
35
  pluralTitle,
30
36
  singularTitle,
31
37
  wsId,
@@ -48,8 +54,14 @@ export function TransactionsCreateSummary({
48
54
  canCreateConfidentialTransactions={
49
55
  canCreateConfidentialTransactions
50
56
  }
57
+ permissionRequestUser={permissionRequestUser}
51
58
  />
52
- ) : undefined
59
+ ) : (
60
+ <FinancePermissionWarningContent
61
+ missingPermissions={['create_transactions']}
62
+ user={permissionRequestUser}
63
+ />
64
+ )
53
65
  }
54
66
  />
55
67
  );
@@ -6,6 +6,7 @@ import { Button } from '@tuturuuu/ui/button';
6
6
  import SearchBar from '@tuturuuu/ui/custom/search-bar';
7
7
  import { Dialog, DialogContent, DialogTrigger } from '@tuturuuu/ui/dialog';
8
8
  import { DateRangeFilterWrapper } from '@tuturuuu/ui/finance/shared/date-range-filter-wrapper';
9
+ import type { FinancePermissionRequestUser } from '@tuturuuu/ui/finance/shared/finance-permission-warning-dialog';
9
10
  import { CategoryFilterWrapper } from '@tuturuuu/ui/finance/transactions/category-filter-wrapper';
10
11
  import { InfiniteTransactionsList } from '@tuturuuu/ui/finance/transactions/infinite-transactions-list';
11
12
  import MoneyLoverImportDialog from '@tuturuuu/ui/finance/transactions/money-lover-import-dialog';
@@ -41,6 +42,7 @@ interface TransactionsInfinitePageProps {
41
42
  canViewConfidentialCategory?: boolean;
42
43
  /** Hide transaction creator (useful for personal workspaces) */
43
44
  isPersonalWorkspace?: boolean;
45
+ permissionRequestUser?: FinancePermissionRequestUser | null;
44
46
  showTransactionTypeFilter?: boolean;
45
47
  }
46
48
 
@@ -62,6 +64,7 @@ export function TransactionsInfinitePage({
62
64
  canViewConfidentialDescription,
63
65
  canViewConfidentialCategory,
64
66
  isPersonalWorkspace,
67
+ permissionRequestUser,
65
68
  showTransactionTypeFilter,
66
69
  }: TransactionsInfinitePageProps) {
67
70
  const t = useTranslations();
@@ -206,6 +209,7 @@ export function TransactionsInfinitePage({
206
209
  canViewConfidentialDescription={canViewConfidentialDescription}
207
210
  canViewConfidentialCategory={canViewConfidentialCategory}
208
211
  isPersonalWorkspace={isPersonalWorkspace}
212
+ permissionRequestUser={permissionRequestUser}
209
213
  />
210
214
  </Suspense>
211
215
  </div>
@@ -1,3 +1,8 @@
1
+ import { Button } from '@tuturuuu/ui/button';
2
+ import {
3
+ type FinancePermissionRequestUser,
4
+ FinancePermissionWarningDialog,
5
+ } from '@tuturuuu/ui/finance/shared/finance-permission-warning-dialog';
1
6
  import ExportDialogContent from '@tuturuuu/ui/finance/transactions/export-dialog-content';
2
7
  import { TransactionsCreateSummary } from '@tuturuuu/ui/finance/transactions/transactions-create-summary';
3
8
  import { TransactionsInfinitePage } from '@tuturuuu/ui/finance/transactions/transactions-infinite-page';
@@ -19,6 +24,7 @@ interface Props {
19
24
  personal?: boolean | null;
20
25
  timezone?: string | null;
21
26
  };
27
+ permissionRequestUser?: FinancePermissionRequestUser | null;
22
28
  openCreateDialog?: boolean;
23
29
  showTransactionTypeFilter?: boolean;
24
30
  }
@@ -28,6 +34,7 @@ export default async function TransactionsPage({
28
34
  permissions,
29
35
  wsId,
30
36
  workspace,
37
+ permissionRequestUser,
31
38
  openCreateDialog = false,
32
39
  showTransactionTypeFilter = false,
33
40
  }: Props) {
@@ -71,7 +78,20 @@ export default async function TransactionsPage({
71
78
  'view_confidential_category'
72
79
  );
73
80
 
74
- if (!canViewTransactions) return notFound();
81
+ if (!canViewTransactions) {
82
+ return (
83
+ <FinancePermissionWarningDialog
84
+ defaultOpen
85
+ missingPermissions={['view_transactions']}
86
+ user={permissionRequestUser}
87
+ trigger={
88
+ <Button variant="outline">
89
+ {t('finance-permission-warning.open_request')}
90
+ </Button>
91
+ }
92
+ />
93
+ );
94
+ }
75
95
 
76
96
  return (
77
97
  <>
@@ -87,6 +107,7 @@ export default async function TransactionsPage({
87
107
  canChangeFinanceWallets={canChangeFinanceWallets}
88
108
  canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
89
109
  canCreateConfidentialTransactions={canCreateConfidentialTransactions}
110
+ permissionRequestUser={permissionRequestUser}
90
111
  />
91
112
  <Separator className="my-4" />
92
113
  <TransactionsInfinitePage
@@ -109,6 +130,7 @@ export default async function TransactionsPage({
109
130
  canViewConfidentialDescription={canViewConfidentialDescription}
110
131
  canViewConfidentialCategory={canViewConfidentialCategory}
111
132
  isPersonalWorkspace={!!resolvedWorkspace.personal}
133
+ permissionRequestUser={permissionRequestUser}
112
134
  showTransactionTypeFilter={showTransactionTypeFilter}
113
135
  />
114
136
  </>
@@ -4,6 +4,7 @@ import { Pencil, Plus } from '@tuturuuu/icons';
4
4
  import type { Wallet } from '@tuturuuu/types/primitives/Wallet';
5
5
  import { Button } from '@tuturuuu/ui/button';
6
6
  import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger';
7
+ import type { FinancePermissionRequestUser } from '@tuturuuu/ui/finance/shared/finance-permission-warning-dialog';
7
8
  import { TransactionForm } from '@tuturuuu/ui/finance/transactions/form';
8
9
  import { WalletForm } from '@tuturuuu/ui/finance/wallets/form';
9
10
  import { useTranslations } from 'next-intl';
@@ -21,6 +22,7 @@ interface WalletDetailsActionsProps {
21
22
  canSetFinanceWalletsOnCreate?: boolean;
22
23
  canDeleteWallets: boolean;
23
24
  isPersonalWorkspace: boolean;
25
+ permissionRequestUser?: FinancePermissionRequestUser | null;
24
26
  }
25
27
 
26
28
  export function WalletDetailsActions({
@@ -34,6 +36,7 @@ export function WalletDetailsActions({
34
36
  canSetFinanceWalletsOnCreate,
35
37
  canDeleteWallets,
36
38
  isPersonalWorkspace,
39
+ permissionRequestUser,
37
40
  }: WalletDetailsActionsProps) {
38
41
  const t = useTranslations();
39
42
 
@@ -101,6 +104,7 @@ export function WalletDetailsActions({
101
104
  canCreateConfidentialTransactions={
102
105
  canCreateConfidentialTransactions
103
106
  }
107
+ permissionRequestUser={permissionRequestUser}
104
108
  />
105
109
  }
106
110
  />
@@ -7,6 +7,7 @@ import {
7
7
  import { createAdminClient } from '@tuturuuu/supabase/next/server';
8
8
  import type { Wallet } from '@tuturuuu/types';
9
9
  import FeatureSummary from '@tuturuuu/ui/custom/feature-summary';
10
+ import type { FinancePermissionRequestUser } from '@tuturuuu/ui/finance/shared/finance-permission-warning-dialog';
10
11
  import { InfiniteTransactionsList } from '@tuturuuu/ui/finance/transactions/infinite-transactions-list';
11
12
  import { Separator } from '@tuturuuu/ui/separator';
12
13
  import { Skeleton } from '@tuturuuu/ui/skeleton';
@@ -47,6 +48,7 @@ interface Props {
47
48
  workspace?: {
48
49
  personal?: boolean | null;
49
50
  };
51
+ permissionRequestUser?: FinancePermissionRequestUser | null;
50
52
  }
51
53
 
52
54
  export default async function WalletDetailsPage({
@@ -56,6 +58,7 @@ export default async function WalletDetailsPage({
56
58
  internalApiOptions,
57
59
  permissions,
58
60
  workspace,
61
+ permissionRequestUser,
59
62
  }: Props) {
60
63
  const [t, resolvedWorkspace, resolvedPermissions, resolvedDefaultCurrency] =
61
64
  await Promise.all([
@@ -164,6 +167,7 @@ export default async function WalletDetailsPage({
164
167
  canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
165
168
  canDeleteWallets={canDeleteWallets}
166
169
  isPersonalWorkspace={!!resolvedWorkspace.personal}
170
+ permissionRequestUser={permissionRequestUser}
167
171
  />
168
172
  </div>
169
173
  <Separator className="my-4" />
@@ -320,6 +324,7 @@ export default async function WalletDetailsPage({
320
324
  canViewConfidentialDescription={canViewConfidentialDescription}
321
325
  canViewConfidentialCategory={canViewConfidentialCategory}
322
326
  isPersonalWorkspace={!!resolvedWorkspace.personal}
327
+ permissionRequestUser={permissionRequestUser}
323
328
  />
324
329
  </Suspense>
325
330
  </div>
@@ -11,6 +11,7 @@ import { AgendaView } from './agenda-view';
11
11
  import { CalendarHeader } from './calendar-header';
12
12
  import { CalendarViewWithTrail } from './calendar-view-with-trail';
13
13
  import { EventModal } from './event-modal';
14
+ import { EventPreviewPopover } from './event-preview-popover';
14
15
  import { MonthCalendar } from './month-calendar';
15
16
  import { useCalendarSettings } from './settings/settings-context';
16
17
  import { WeekdayBar } from './weekday-bar';
@@ -664,7 +665,14 @@ export const CalendarContent = ({
664
665
  )}
665
666
  </div>
666
667
 
667
- {disabled ? null : workspace && <EventModal />}
668
+ {disabled
669
+ ? null
670
+ : workspace && (
671
+ <>
672
+ <EventPreviewPopover />
673
+ <EventModal />
674
+ </>
675
+ )}
668
676
  </div>
669
677
  );
670
678
  };
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { zodResolver } from '@hookform/resolvers/zod';
4
+ import { useQuery } from '@tanstack/react-query';
4
5
  import { calendarEventsSchema } from '@tuturuuu/ai/calendar/events';
5
6
  import { useObject } from '@tuturuuu/ai/object/core';
6
7
  import {
@@ -24,6 +25,11 @@ import {
24
25
  Unlock,
25
26
  X,
26
27
  } from '@tuturuuu/icons';
28
+ import {
29
+ type CalendarSourceInput,
30
+ type CalendarSourceOption,
31
+ getWorkspaceCalendarDefaultSource,
32
+ } from '@tuturuuu/internal-api';
27
33
  import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
28
34
  import type { SupportedColor } from '@tuturuuu/types/primitives/SupportedColors';
29
35
  import { Badge } from '@tuturuuu/ui/badge';
@@ -48,6 +54,13 @@ import { useCalendar } from '@tuturuuu/ui/hooks/use-calendar';
48
54
  import { useForm } from '@tuturuuu/ui/hooks/use-form';
49
55
  import { useToast } from '@tuturuuu/ui/hooks/use-toast';
50
56
  import { ScrollArea } from '@tuturuuu/ui/scroll-area';
57
+ import {
58
+ Select,
59
+ SelectContent,
60
+ SelectItem,
61
+ SelectTrigger,
62
+ SelectValue,
63
+ } from '@tuturuuu/ui/select';
51
64
  import { Separator } from '@tuturuuu/ui/separator';
52
65
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs';
53
66
  import {
@@ -61,7 +74,7 @@ import dayjs from 'dayjs';
61
74
  import ts from 'dayjs/plugin/timezone';
62
75
  import utc from 'dayjs/plugin/utc';
63
76
  import Image from 'next/image';
64
- import { useCallback, useEffect, useRef, useState } from 'react';
77
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
65
78
  import { z } from 'zod';
66
79
  import { Alert, AlertDescription, AlertTitle } from '../../alert';
67
80
  import { AutosizeTextarea } from '../../custom/autosize-textarea';
@@ -89,6 +102,53 @@ const AIFormSchema = z.object({
89
102
  smart_scheduling: z.boolean().default(true),
90
103
  });
91
104
 
105
+ function sourceInputFromOption(
106
+ option?: CalendarSourceOption | null
107
+ ): CalendarSourceInput | undefined {
108
+ if (!option) return undefined;
109
+
110
+ if (option.provider === 'tuturuuu') {
111
+ return {
112
+ provider: 'tuturuuu',
113
+ workspaceCalendarId: option.workspaceCalendarId,
114
+ };
115
+ }
116
+
117
+ return {
118
+ provider: option.provider,
119
+ connectionId: option.connectionId,
120
+ };
121
+ }
122
+
123
+ function findEventSourceOption(
124
+ options: CalendarSourceOption[],
125
+ event: Partial<CalendarEvent>
126
+ ) {
127
+ if (event.provider === 'google' || event.provider === 'microsoft') {
128
+ return options.find((option) => {
129
+ if (option.provider === 'tuturuuu') return false;
130
+ if (option.provider !== event.provider) return false;
131
+ if (
132
+ event.source_calendar_id &&
133
+ option.workspaceCalendarId === event.source_calendar_id
134
+ ) {
135
+ return true;
136
+ }
137
+
138
+ return (
139
+ option.externalCalendarId ===
140
+ (event.external_calendar_id ?? event.google_calendar_id)
141
+ );
142
+ });
143
+ }
144
+
145
+ return options.find(
146
+ (option) =>
147
+ option.provider === 'tuturuuu' &&
148
+ option.workspaceCalendarId === event.source_calendar_id
149
+ );
150
+ }
151
+
92
152
  export function EventModal() {
93
153
  const { toast } = useToast();
94
154
  const startPickerRef = useRef<HTMLButtonElement>(null);
@@ -118,6 +178,23 @@ export function EventModal() {
118
178
  location: '',
119
179
  locked: false,
120
180
  });
181
+ const [selectedSourceId, setSelectedSourceId] = useState<string | null>(null);
182
+
183
+ const wsId = activeEvent?.ws_id || event.ws_id;
184
+ const { data: sourceData } = useQuery({
185
+ queryKey: ['calendar-default-source', wsId],
186
+ enabled: !!wsId,
187
+ queryFn: () => getWorkspaceCalendarDefaultSource(wsId as string),
188
+ staleTime: 30_000,
189
+ });
190
+ const sourceOptions = useMemo(
191
+ () => sourceData?.options ?? [],
192
+ [sourceData?.options]
193
+ );
194
+ const selectedSourceOption =
195
+ sourceOptions.find((option) => option.id === selectedSourceId) ??
196
+ sourceData?.defaultSource ??
197
+ sourceOptions[0];
121
198
 
122
199
  // State for AI event generation
123
200
  const [generatedEvents, setGeneratedEvents] =
@@ -239,10 +316,19 @@ export function EventModal() {
239
316
  location: activeEvent.location || '',
240
317
  locked: activeEvent.locked || false,
241
318
  ws_id: activeEvent.ws_id,
319
+ provider: activeEvent.provider,
320
+ source_calendar_id: activeEvent.source_calendar_id,
321
+ external_calendar_id: activeEvent.external_calendar_id,
322
+ external_event_id: activeEvent.external_event_id,
242
323
  google_event_id: activeEvent.google_event_id,
324
+ google_calendar_id: activeEvent.google_calendar_id,
243
325
  };
244
326
 
245
327
  setEvent(cleanEventData);
328
+ const sourceOption = findEventSourceOption(sourceOptions, cleanEventData);
329
+ setSelectedSourceId(
330
+ sourceOption?.id ?? sourceData?.defaultSource?.id ?? null
331
+ );
246
332
 
247
333
  // Only check for all-day if this is an existing event (not a new one)
248
334
  if (activeEvent.id !== 'new') {
@@ -270,9 +356,11 @@ export function EventModal() {
270
356
  color: 'BLUE' as SupportedColor,
271
357
  location: '',
272
358
  locked: false,
359
+ ws_id: '',
273
360
  };
274
361
 
275
362
  setEvent(newEvent);
363
+ setSelectedSourceId(sourceData?.defaultSource?.id ?? null);
276
364
  setIsAllDay(false);
277
365
 
278
366
  // Reset AI form
@@ -282,7 +370,15 @@ export function EventModal() {
282
370
 
283
371
  // Clear any error messages
284
372
  setDateError(null);
285
- }, [activeEvent, checkForOverlaps, aiForm, defaultNewEventTab, isEditing]);
373
+ }, [
374
+ activeEvent,
375
+ checkForOverlaps,
376
+ aiForm,
377
+ defaultNewEventTab,
378
+ isEditing,
379
+ sourceData?.defaultSource?.id,
380
+ sourceOptions,
381
+ ]);
286
382
 
287
383
  // Handle manual event save
288
384
  const handleManualSave = async () => {
@@ -309,6 +405,7 @@ export function EventModal() {
309
405
  color: event.color || 'BLUE',
310
406
  location: event.location || '',
311
407
  locked: event.locked || false,
408
+ source: sourceInputFromOption(selectedSourceOption),
312
409
  };
313
410
 
314
411
  if (activeEvent?.id === 'new') {
@@ -413,6 +510,7 @@ export function EventModal() {
413
510
  color: eventData.color || 'BLUE',
414
511
  location: eventData.location || '',
415
512
  locked: eventData.locked || false,
513
+ source: sourceInputFromOption(selectedSourceOption),
416
514
  };
417
515
 
418
516
  const savedEvent = await addEvent(calendarEvent);
@@ -927,6 +1025,52 @@ export function EventModal() {
927
1025
  onChange={(value) => setEvent({ ...event, title: value })}
928
1026
  />
929
1027
 
1028
+ <div className="space-y-2">
1029
+ <div className="flex items-center justify-between">
1030
+ <label
1031
+ htmlFor="event-source"
1032
+ className="font-medium text-sm"
1033
+ >
1034
+ Calendar source
1035
+ </label>
1036
+ {selectedSourceOption && (
1037
+ <Badge variant="secondary" className="capitalize">
1038
+ {selectedSourceOption.provider}
1039
+ </Badge>
1040
+ )}
1041
+ </div>
1042
+ <Select
1043
+ value={selectedSourceId ?? undefined}
1044
+ onValueChange={(value) => setSelectedSourceId(value)}
1045
+ disabled={sourceOptions.length === 0 || isSaving}
1046
+ >
1047
+ <SelectTrigger id="event-source" className="w-full">
1048
+ <SelectValue placeholder="Choose calendar source" />
1049
+ </SelectTrigger>
1050
+ <SelectContent>
1051
+ {sourceOptions.map((option) => (
1052
+ <SelectItem key={option.id} value={option.id}>
1053
+ <div className="flex min-w-0 items-center gap-2">
1054
+ <span
1055
+ className="h-2.5 w-2.5 shrink-0 rounded-full"
1056
+ style={{
1057
+ backgroundColor: option.color ?? undefined,
1058
+ }}
1059
+ />
1060
+ <span className="truncate">{option.label}</span>
1061
+ {option.provider !== 'tuturuuu' &&
1062
+ option.accountEmail && (
1063
+ <span className="text-muted-foreground text-xs">
1064
+ {option.accountEmail}
1065
+ </span>
1066
+ )}
1067
+ </div>
1068
+ </SelectItem>
1069
+ ))}
1070
+ </SelectContent>
1071
+ </Select>
1072
+ </div>
1073
+
930
1074
  {/* Date and Time Selection */}
931
1075
  <div className="space-y-2">
932
1076
  <div className="flex items-center justify-between">
@@ -0,0 +1,200 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Calendar,
5
+ Clock,
6
+ Edit3,
7
+ ExternalLink,
8
+ Lock,
9
+ MapPin,
10
+ Trash2,
11
+ X,
12
+ } from '@tuturuuu/icons';
13
+ import { Badge } from '@tuturuuu/ui/badge';
14
+ import { Button } from '@tuturuuu/ui/button';
15
+ import { useCalendar } from '@tuturuuu/ui/hooks/use-calendar';
16
+ import { cn } from '@tuturuuu/utils/format';
17
+ import { format } from 'date-fns';
18
+ import { useEffect, useRef } from 'react';
19
+
20
+ function formatEventTime(startAt?: string, endAt?: string) {
21
+ if (!startAt || !endAt) return '';
22
+
23
+ const start = new Date(startAt);
24
+ const end = new Date(endAt);
25
+ const sameDay = start.toDateString() === end.toDateString();
26
+
27
+ if (sameDay) {
28
+ return `${format(start, 'EEE, MMM d')} - ${format(start, 'p')} - ${format(end, 'p')}`;
29
+ }
30
+
31
+ return `${format(start, 'MMM d, p')} - ${format(end, 'MMM d, p')}`;
32
+ }
33
+
34
+ function getSourceLabel(event: {
35
+ provider?: string | null;
36
+ google_calendar_id?: string | null;
37
+ external_calendar_id?: string | null;
38
+ _calendarName?: string;
39
+ }) {
40
+ if (event._calendarName) return event._calendarName;
41
+ if (event.provider === 'google') {
42
+ return event.google_calendar_id || event.external_calendar_id || 'Google';
43
+ }
44
+ if (event.provider === 'microsoft') {
45
+ return event.external_calendar_id || 'Microsoft';
46
+ }
47
+ return 'Tuturuuu';
48
+ }
49
+
50
+ export function EventPreviewPopover() {
51
+ const {
52
+ previewEvent,
53
+ isPreviewOpen,
54
+ closePreview,
55
+ openEventEditor,
56
+ deleteEvent,
57
+ readOnly,
58
+ } = useCalendar();
59
+ const popoverRef = useRef<HTMLDivElement>(null);
60
+
61
+ useEffect(() => {
62
+ if (!isPreviewOpen) return;
63
+
64
+ const handlePointerDown = (event: PointerEvent) => {
65
+ if (
66
+ popoverRef.current &&
67
+ event.target instanceof Node &&
68
+ !popoverRef.current.contains(event.target)
69
+ ) {
70
+ closePreview();
71
+ }
72
+ };
73
+
74
+ const handleKeyDown = (event: KeyboardEvent) => {
75
+ if (event.key === 'Escape') closePreview();
76
+ };
77
+
78
+ document.addEventListener('pointerdown', handlePointerDown);
79
+ document.addEventListener('keydown', handleKeyDown);
80
+ return () => {
81
+ document.removeEventListener('pointerdown', handlePointerDown);
82
+ document.removeEventListener('keydown', handleKeyDown);
83
+ };
84
+ }, [closePreview, isPreviewOpen]);
85
+
86
+ if (!isPreviewOpen || !previewEvent) return null;
87
+
88
+ const provider = previewEvent.provider || 'tuturuuu';
89
+ const isSynced = provider === 'google' || provider === 'microsoft';
90
+ const sourceLabel = getSourceLabel(previewEvent);
91
+
92
+ return (
93
+ <div
94
+ ref={popoverRef}
95
+ className={cn(
96
+ 'fixed top-20 right-4 z-50 w-[min(380px,calc(100vw-2rem))]',
97
+ 'rounded-lg border bg-popover p-4 text-popover-foreground shadow-xl'
98
+ )}
99
+ role="dialog"
100
+ aria-label="Event quick preview"
101
+ >
102
+ <div className="flex items-start gap-3">
103
+ <div
104
+ className="mt-1 h-3 w-3 shrink-0 rounded-full"
105
+ style={{ backgroundColor: previewEvent._calendarColor || undefined }}
106
+ />
107
+ <div className="min-w-0 flex-1 space-y-3">
108
+ <div className="flex items-start justify-between gap-3">
109
+ <div className="min-w-0">
110
+ <h3 className="truncate font-semibold text-base">
111
+ {previewEvent.title || 'Untitled event'}
112
+ </h3>
113
+ <p className="text-muted-foreground text-sm">
114
+ {formatEventTime(previewEvent.start_at, previewEvent.end_at)}
115
+ </p>
116
+ </div>
117
+ <Button
118
+ type="button"
119
+ variant="ghost"
120
+ size="icon"
121
+ className="h-8 w-8 shrink-0"
122
+ onClick={closePreview}
123
+ >
124
+ <X className="h-4 w-4" />
125
+ </Button>
126
+ </div>
127
+
128
+ <div className="space-y-2 text-sm">
129
+ <div className="flex items-center gap-2 text-muted-foreground">
130
+ <Calendar className="h-4 w-4" />
131
+ <span className="truncate">{sourceLabel}</span>
132
+ <Badge variant="secondary" className="ml-auto capitalize">
133
+ {provider}
134
+ </Badge>
135
+ </div>
136
+ <div className="flex items-center gap-2 text-muted-foreground">
137
+ <Clock className="h-4 w-4" />
138
+ <span>
139
+ {isSynced ? 'Synced with provider' : 'Local calendar'}
140
+ </span>
141
+ </div>
142
+ {previewEvent.locked && (
143
+ <div className="flex items-center gap-2 text-muted-foreground">
144
+ <Lock className="h-4 w-4" />
145
+ <span>Locked from auto scheduling</span>
146
+ </div>
147
+ )}
148
+ {previewEvent.location && (
149
+ <div className="flex items-center gap-2 text-muted-foreground">
150
+ <MapPin className="h-4 w-4" />
151
+ <span className="truncate">{previewEvent.location}</span>
152
+ </div>
153
+ )}
154
+ </div>
155
+
156
+ {previewEvent.description && (
157
+ <p className="line-clamp-3 text-muted-foreground text-sm">
158
+ {previewEvent.description}
159
+ </p>
160
+ )}
161
+
162
+ <div className="flex items-center justify-end gap-2 pt-1">
163
+ {isSynced && (
164
+ <Badge variant="outline" className="gap-1">
165
+ <ExternalLink className="h-3 w-3" />
166
+ {provider}
167
+ </Badge>
168
+ )}
169
+ {!readOnly && (
170
+ <Button
171
+ type="button"
172
+ variant="ghost"
173
+ size="icon"
174
+ className="h-9 w-9 text-destructive hover:text-destructive"
175
+ onClick={async () => {
176
+ await deleteEvent(previewEvent.id);
177
+ closePreview();
178
+ }}
179
+ >
180
+ <Trash2 className="h-4 w-4" />
181
+ </Button>
182
+ )}
183
+ <Button
184
+ type="button"
185
+ size="sm"
186
+ className="gap-2"
187
+ onClick={() => {
188
+ openEventEditor(previewEvent.id);
189
+ closePreview();
190
+ }}
191
+ >
192
+ <Edit3 className="h-4 w-4" />
193
+ Edit
194
+ </Button>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ );
200
+ }