@tuturuuu/ui 0.8.0 → 0.10.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.
- package/CHANGELOG.md +69 -0
- package/biome.json +1 -1
- package/package.json +74 -71
- package/src/components/ui/accordion.tsx +1 -1
- package/src/components/ui/breadcrumb.tsx +1 -1
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
- package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
- package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
- package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/carousel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
- package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
- package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
- package/src/components/ui/checkbox.tsx +1 -1
- package/src/components/ui/color-picker.tsx +1 -1
- package/src/components/ui/command.tsx +1 -1
- package/src/components/ui/context-menu.tsx +5 -1
- package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +46 -1
- package/src/components/ui/custom/combobox.test.tsx +195 -0
- package/src/components/ui/custom/combobox.tsx +273 -156
- package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
- package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
- package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
- package/src/components/ui/custom/nav-link.test.tsx +165 -0
- package/src/components/ui/custom/nav-link.tsx +69 -11
- package/src/components/ui/custom/navigation.tsx +1 -0
- package/src/components/ui/custom/settings/task-settings.tsx +104 -0
- package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
- package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
- package/src/components/ui/custom/settings-dialog-search.ts +75 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +65 -28
- package/src/components/ui/custom/theme-toggle.tsx +1 -1
- package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
- package/src/components/ui/custom/workspace-select.tsx +25 -19
- package/src/components/ui/dialog.test.tsx +52 -0
- package/src/components/ui/dialog.tsx +6 -2
- package/src/components/ui/dropdown-menu.tsx +5 -1
- package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
- package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
- package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
- package/src/components/ui/finance/debts/debts-page.tsx +15 -2
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
- package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/utils.ts +3 -1
- package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
- package/src/components/ui/finance/transactions/form-types.ts +1 -0
- package/src/components/ui/finance/transactions/form.tsx +2 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
- package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
- package/src/components/ui/finance/wallets/form.test.tsx +51 -3
- package/src/components/ui/finance/wallets/form.tsx +15 -4
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
- package/src/components/ui/input-otp.tsx +1 -1
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
- package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
- package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
- package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
- package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
- package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
- package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
- package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
- package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
- package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
- package/src/components/ui/navigation-menu.tsx +1 -1
- package/src/components/ui/pagination.tsx +1 -1
- package/src/components/ui/radio-group.tsx +1 -1
- package/src/components/ui/select.tsx +5 -1
- package/src/components/ui/sheet.tsx +1 -1
- package/src/components/ui/sidebar.tsx +1 -1
- package/src/components/ui/storefront/cart-popover.tsx +61 -0
- package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
- package/src/components/ui/storefront/cart-summary.tsx +93 -154
- package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
- package/src/components/ui/storefront/listing-card.tsx +1 -1
- package/src/components/ui/storefront/merch-sections.tsx +70 -0
- package/src/components/ui/storefront/product-detail.tsx +1 -1
- package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
- package/src/components/ui/storefront/storefront-surface.tsx +101 -166
- package/src/components/ui/storefront/types.ts +4 -0
- package/src/components/ui/storefront/utils.ts +6 -0
- package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
- package/src/components/ui/text-editor/background-color-extension.ts +62 -0
- package/src/components/ui/text-editor/color-controls.tsx +284 -0
- package/src/components/ui/text-editor/editor.tsx +69 -14
- package/src/components/ui/text-editor/extensions.ts +8 -2
- package/src/components/ui/text-editor/highlight-extension.ts +22 -0
- package/src/components/ui/text-editor/tool-bar.tsx +9 -16
- package/src/components/ui/toast.tsx +1 -1
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +286 -0
- package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
- package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +15 -226
- package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +121 -39
- package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +642 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +224 -15
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +535 -53
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +101 -33
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +235 -113
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +50 -5
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +12 -2
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +10 -1
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +271 -36
- package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
- package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +22 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
- package/src/components/ui/tu-do/boards/form.tsx +1 -1
- package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
- package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
- package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
- package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
- package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
- package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
- package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
- package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
- package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
- package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
- package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
- package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +141 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +377 -36
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +374 -0
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +419 -5
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +38 -0
- package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
- package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
- package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +15 -10
- package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
- package/src/components/ui/tu-do/shared/board-header.tsx +471 -975
- package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
- package/src/components/ui/tu-do/shared/board-switcher.tsx +244 -220
- package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
- package/src/components/ui/tu-do/shared/board-views.tsx +577 -85
- package/src/components/ui/tu-do/shared/list-view.tsx +246 -2
- package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
- package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
- package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
- package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +11 -1
- package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
- package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
- package/src/declarations.d.ts +1 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
- package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
- package/src/hooks/use-calendar-sync.tsx +247 -243
- package/src/hooks/use-calendar.tsx +323 -138
- package/src/hooks/use-task-actions.ts +24 -0
- package/src/hooks/use-user-workspace-config.ts +75 -0
- package/src/hooks/use-workspace-currency.ts +8 -3
- package/src/hooks/useBoardPresence.ts +364 -0
- package/src/hooks/useBoardRealtimeEventHandler.ts +45 -90
- package/src/lib/workspace-actions.ts +2 -6
|
@@ -4,6 +4,7 @@ import { useCalendarSync } from '@tuturuuu/ui/hooks/use-calendar-sync';
|
|
|
4
4
|
import dayjs from 'dayjs';
|
|
5
5
|
import timezone from 'dayjs/plugin/timezone';
|
|
6
6
|
import { useParams } from 'next/navigation';
|
|
7
|
+
import { useMemo } from 'react';
|
|
7
8
|
import { CalendarColumn } from './calendar-column';
|
|
8
9
|
import { DAY_HEIGHT, MAX_LEVEL } from './config';
|
|
9
10
|
import { EventCard } from './event-card';
|
|
@@ -12,6 +13,146 @@ import { useCalendarSettings } from './settings/settings-context';
|
|
|
12
13
|
|
|
13
14
|
dayjs.extend(timezone);
|
|
14
15
|
|
|
16
|
+
type LayoutCalendarEvent = CalendarEvent & {
|
|
17
|
+
_dayKey: string;
|
|
18
|
+
_endMs: number;
|
|
19
|
+
_startMs: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getDayKeyFromDate(date: Date, tz?: string) {
|
|
23
|
+
return (tz === 'auto' ? dayjs(date) : dayjs(date).tz(tz)).format(
|
|
24
|
+
'YYYY-MM-DD'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getDayKeyFromIso(value: string, tz?: string) {
|
|
29
|
+
return (tz === 'auto' ? dayjs(value) : dayjs(value).tz(tz)).format(
|
|
30
|
+
'YYYY-MM-DD'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function withLayoutMetadata(
|
|
35
|
+
event: CalendarEvent,
|
|
36
|
+
tz?: string
|
|
37
|
+
): LayoutCalendarEvent {
|
|
38
|
+
return {
|
|
39
|
+
...event,
|
|
40
|
+
_dayKey: getDayKeyFromIso(event.start_at, tz),
|
|
41
|
+
_endMs: new Date(event.end_at).getTime(),
|
|
42
|
+
_startMs: new Date(event.start_at).getTime(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assignEventLayout(
|
|
47
|
+
visibleEvents: CalendarEvent[],
|
|
48
|
+
tz?: string
|
|
49
|
+
): CalendarEvent[] {
|
|
50
|
+
const sortedEvents = visibleEvents
|
|
51
|
+
.map((event) => withLayoutMetadata(event, tz))
|
|
52
|
+
.sort((left, right) => left._startMs - right._startMs);
|
|
53
|
+
|
|
54
|
+
const eventLevels = new Map<string, number>();
|
|
55
|
+
const eventOverlapCounts = new Map<string, number>();
|
|
56
|
+
const eventOverlapGroups = new Map<string, string[]>();
|
|
57
|
+
const eventColumns = new Map<string, number>();
|
|
58
|
+
const eventsByDay = new Map<string, LayoutCalendarEvent[]>();
|
|
59
|
+
|
|
60
|
+
for (const event of sortedEvents) {
|
|
61
|
+
const dayEvents = eventsByDay.get(event._dayKey) ?? [];
|
|
62
|
+
dayEvents.push(event);
|
|
63
|
+
eventsByDay.set(event._dayKey, dayEvents);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const dayEvents of eventsByDay.values()) {
|
|
67
|
+
const sortedDayEvents = [...dayEvents].sort(
|
|
68
|
+
(left, right) => left._startMs - right._startMs
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const overlapGroups: LayoutCalendarEvent[][] = [];
|
|
72
|
+
let activeGroup: LayoutCalendarEvent[] = [];
|
|
73
|
+
let activeGroupEnd = Number.NEGATIVE_INFINITY;
|
|
74
|
+
|
|
75
|
+
for (const event of sortedDayEvents) {
|
|
76
|
+
if (activeGroup.length === 0 || event._startMs < activeGroupEnd) {
|
|
77
|
+
activeGroup.push(event);
|
|
78
|
+
activeGroupEnd = Math.max(activeGroupEnd, event._endMs);
|
|
79
|
+
} else {
|
|
80
|
+
overlapGroups.push(activeGroup);
|
|
81
|
+
activeGroup = [event];
|
|
82
|
+
activeGroupEnd = event._endMs;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (activeGroup.length > 0) overlapGroups.push(activeGroup);
|
|
87
|
+
|
|
88
|
+
for (const group of overlapGroups) {
|
|
89
|
+
const sortedGroup = [...group].sort((left, right) => {
|
|
90
|
+
if (left._startMs !== right._startMs)
|
|
91
|
+
return left._startMs - right._startMs;
|
|
92
|
+
return right._endMs - right._startMs - (left._endMs - left._startMs);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const groupEventColumns = new Map<string, number>();
|
|
96
|
+
const columnEndTimes: number[] = [];
|
|
97
|
+
|
|
98
|
+
for (const event of sortedGroup) {
|
|
99
|
+
let column = columnEndTimes.findIndex(
|
|
100
|
+
(columnEndTime) => event._startMs >= columnEndTime
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (column === -1) column = columnEndTimes.length;
|
|
104
|
+
|
|
105
|
+
groupEventColumns.set(event.id, column);
|
|
106
|
+
columnEndTimes[column] = event._endMs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const maxColumn = Math.max(0, ...groupEventColumns.values());
|
|
110
|
+
const orderedEventIds: string[] = [];
|
|
111
|
+
|
|
112
|
+
for (let column = 0; column <= maxColumn; column++) {
|
|
113
|
+
const columnEvents = sortedGroup
|
|
114
|
+
.filter((event) => groupEventColumns.get(event.id) === column)
|
|
115
|
+
.sort(
|
|
116
|
+
(left, right) =>
|
|
117
|
+
right._endMs - right._startMs - (left._endMs - left._startMs)
|
|
118
|
+
);
|
|
119
|
+
orderedEventIds.push(...columnEvents.map((event) => event.id));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const event of sortedGroup) {
|
|
123
|
+
eventOverlapCounts.set(event.id, sortedGroup.length);
|
|
124
|
+
eventOverlapGroups.set(event.id, orderedEventIds);
|
|
125
|
+
eventColumns.set(event.id, groupEventColumns.get(event.id) ?? 0);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const levelEndTimes: number[] = [];
|
|
130
|
+
|
|
131
|
+
for (const event of sortedDayEvents) {
|
|
132
|
+
let level = 0;
|
|
133
|
+
while (
|
|
134
|
+
level < MAX_LEVEL &&
|
|
135
|
+
levelEndTimes[level] !== undefined &&
|
|
136
|
+
event._startMs < levelEndTimes[level]!
|
|
137
|
+
) {
|
|
138
|
+
level++;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
level = Math.min(level, MAX_LEVEL - 1);
|
|
142
|
+
eventLevels.set(event.id, level);
|
|
143
|
+
levelEndTimes[level] = event._endMs;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return sortedEvents.map((event) => ({
|
|
148
|
+
...event,
|
|
149
|
+
_column: eventColumns.get(event.id) ?? 0,
|
|
150
|
+
_level: eventLevels.get(event.id) ?? 0,
|
|
151
|
+
_overlapCount: eventOverlapCounts.get(event.id) ?? 1,
|
|
152
|
+
_overlapGroup: eventOverlapGroups.get(event.id) ?? [event.id],
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
15
156
|
export const CalendarMatrix = ({
|
|
16
157
|
dates,
|
|
17
158
|
overlay,
|
|
@@ -55,250 +196,47 @@ export const CalendarEventMatrix = ({ dates }: { dates: Date[] }) => {
|
|
|
55
196
|
useCalendar();
|
|
56
197
|
const tz = settings?.timezone?.timezone;
|
|
57
198
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
? eventsWithoutAllDays.filter(
|
|
62
|
-
(e) => !affectedEventIds.has(e.id) || e._isPreview
|
|
63
|
-
)
|
|
64
|
-
: eventsWithoutAllDays;
|
|
65
|
-
|
|
66
|
-
// Merge real events with preview events for visual demo
|
|
67
|
-
// Deduplicate by ID: if a preview event has the same ID as a real event,
|
|
68
|
-
// prefer the real event (this happens when locked events are included in preview)
|
|
69
|
-
const realEventIds = new Set(filteredRealEvents.map((e) => e.id));
|
|
70
|
-
const filteredPreviewEvents = previewEvents.filter(
|
|
71
|
-
(e) => !realEventIds.has(e.id)
|
|
199
|
+
const visibleDayKeys = useMemo(
|
|
200
|
+
() => new Set(dates.map((date) => getDayKeyFromDate(date, tz))),
|
|
201
|
+
[dates, tz]
|
|
72
202
|
);
|
|
73
|
-
const allEvents = [...filteredRealEvents, ...filteredPreviewEvents];
|
|
74
203
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
204
|
+
const filteredRealEvents = useMemo(
|
|
205
|
+
() =>
|
|
206
|
+
hideNonPreviewEvents
|
|
207
|
+
? eventsWithoutAllDays.filter(
|
|
208
|
+
(event) => !affectedEventIds.has(event.id) || event._isPreview
|
|
209
|
+
)
|
|
210
|
+
: eventsWithoutAllDays,
|
|
211
|
+
[affectedEventIds, eventsWithoutAllDays, hideNonPreviewEvents]
|
|
79
212
|
);
|
|
80
213
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// Simple algorithm to assign levels to events
|
|
98
|
-
const assignLevels = () => {
|
|
99
|
-
// Sort events by start time
|
|
100
|
-
const sortedEvents = [...visibleEvents].sort((a, b) => {
|
|
101
|
-
const aStart = new Date(a.start_at).getTime();
|
|
102
|
-
const bStart = new Date(b.start_at).getTime();
|
|
103
|
-
return aStart - bStart;
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Create maps to store event data
|
|
107
|
-
const eventLevels = new Map<string, number>();
|
|
108
|
-
const eventOverlapCounts = new Map<string, number>();
|
|
109
|
-
const eventOverlapGroups = new Map<string, string[]>();
|
|
110
|
-
const eventColumns = new Map<string, number>();
|
|
111
|
-
|
|
112
|
-
// Group events by day
|
|
113
|
-
const eventsByDay = new Map<string, CalendarEvent[]>();
|
|
114
|
-
|
|
115
|
-
// Populate the day groups
|
|
116
|
-
sortedEvents.forEach((event) => {
|
|
117
|
-
const date = new Date(event.start_at);
|
|
118
|
-
const dateKey = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
|
119
|
-
|
|
120
|
-
if (!eventsByDay.has(dateKey)) {
|
|
121
|
-
eventsByDay.set(dateKey, []);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const dayEvents = eventsByDay.get(dateKey);
|
|
125
|
-
if (dayEvents) {
|
|
126
|
-
dayEvents.push(event);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Process each day's events
|
|
131
|
-
eventsByDay.forEach((dayEvents) => {
|
|
132
|
-
// Sort by start time
|
|
133
|
-
const sortedDayEvents = [...dayEvents].sort((a, b) => {
|
|
134
|
-
const aStart = new Date(a.start_at).getTime();
|
|
135
|
-
const bStart = new Date(b.start_at).getTime();
|
|
136
|
-
return aStart - bStart;
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Track end times for each level
|
|
140
|
-
const levelEndTimes: number[] = [];
|
|
141
|
-
|
|
142
|
-
// Create a more accurate grouping of overlapping events
|
|
143
|
-
// Helper function to check if two events overlap
|
|
144
|
-
const eventsOverlap = (event1: CalendarEvent, event2: CalendarEvent) => {
|
|
145
|
-
const event1Start = new Date(event1.start_at).getTime();
|
|
146
|
-
const event1End = new Date(event1.end_at).getTime();
|
|
147
|
-
const event2Start = new Date(event2.start_at).getTime();
|
|
148
|
-
const event2End = new Date(event2.end_at).getTime();
|
|
149
|
-
|
|
150
|
-
return event1Start < event2End && event1End > event2Start;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
// Create overlap groups
|
|
154
|
-
const overlapGroups: CalendarEvent[][] = [];
|
|
155
|
-
|
|
156
|
-
// Process each event to find its overlap group
|
|
157
|
-
sortedDayEvents.forEach((event) => {
|
|
158
|
-
// Find all groups this event overlaps with
|
|
159
|
-
const overlappingGroupIndices: number[] = [];
|
|
160
|
-
|
|
161
|
-
for (let i = 0; i < overlapGroups.length; i++) {
|
|
162
|
-
const group = overlapGroups[i];
|
|
163
|
-
// Check if event overlaps with any event in this group
|
|
164
|
-
if (group?.some((groupEvent) => eventsOverlap(event, groupEvent))) {
|
|
165
|
-
overlappingGroupIndices.push(i);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (overlappingGroupIndices.length === 0) {
|
|
170
|
-
// No overlapping groups, create a new one
|
|
171
|
-
overlapGroups.push([event]);
|
|
172
|
-
} else {
|
|
173
|
-
// Merge all overlapping groups and add this event
|
|
174
|
-
const newGroup = [event];
|
|
175
|
-
|
|
176
|
-
// Sort indices in descending order to safely remove from array
|
|
177
|
-
overlappingGroupIndices.sort((a, b) => b - a);
|
|
178
|
-
|
|
179
|
-
// Merge all overlapping groups
|
|
180
|
-
overlappingGroupIndices.forEach((index) => {
|
|
181
|
-
newGroup.push(...(overlapGroups[index] ?? []));
|
|
182
|
-
overlapGroups.splice(index, 1);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Add the merged group
|
|
186
|
-
overlapGroups.push(newGroup);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// Now assign column positions using a graph coloring approach
|
|
191
|
-
overlapGroups.forEach((group) => {
|
|
192
|
-
// For each group, assign column positions
|
|
193
|
-
// Column 0 = all non-overlapping events with each other
|
|
194
|
-
// Column 1+ = events that overlap with column 0
|
|
195
|
-
|
|
196
|
-
// Sort group by start time, then duration (longest first)
|
|
197
|
-
const sortedGroup = [...group].sort((a, b) => {
|
|
198
|
-
const aStart = new Date(a.start_at).getTime();
|
|
199
|
-
const bStart = new Date(b.start_at).getTime();
|
|
200
|
-
if (aStart !== bStart) return aStart - bStart;
|
|
201
|
-
|
|
202
|
-
const aDuration =
|
|
203
|
-
new Date(a.end_at).getTime() - new Date(a.start_at).getTime();
|
|
204
|
-
const bDuration =
|
|
205
|
-
new Date(b.end_at).getTime() - new Date(b.start_at).getTime();
|
|
206
|
-
|
|
207
|
-
// Longer events first
|
|
208
|
-
return bDuration - aDuration;
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Assign columns using greedy coloring
|
|
212
|
-
const groupEventColumns = new Map<string, number>();
|
|
213
|
-
const columnEndTimes: number[] = [];
|
|
214
|
-
|
|
215
|
-
sortedGroup.forEach((event) => {
|
|
216
|
-
const eventStart = new Date(event.start_at).getTime();
|
|
217
|
-
const eventEnd = new Date(event.end_at).getTime();
|
|
218
|
-
|
|
219
|
-
// Find the first column where this event can fit
|
|
220
|
-
let column = -1;
|
|
221
|
-
for (let i = 0; i < columnEndTimes.length; i++) {
|
|
222
|
-
if (eventStart >= columnEndTimes[i]!) {
|
|
223
|
-
column = i;
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// If no existing column works, create a new one
|
|
229
|
-
if (column === -1) {
|
|
230
|
-
column = columnEndTimes.length;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
groupEventColumns.set(event.id, column);
|
|
234
|
-
columnEndTimes[column] = eventEnd;
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// Now create the ordered list based on column assignment
|
|
238
|
-
// Column 0 events first (sorted by duration), then column 1, etc.
|
|
239
|
-
const maxColumn = Math.max(...Array.from(groupEventColumns.values()));
|
|
240
|
-
const orderedEventIds: string[] = [];
|
|
241
|
-
|
|
242
|
-
for (let col = 0; col <= maxColumn; col++) {
|
|
243
|
-
const colEvents = sortedGroup
|
|
244
|
-
.filter((e) => groupEventColumns.get(e.id) === col)
|
|
245
|
-
.sort((a, b) => {
|
|
246
|
-
const aDuration =
|
|
247
|
-
new Date(a.end_at).getTime() - new Date(a.start_at).getTime();
|
|
248
|
-
const bDuration =
|
|
249
|
-
new Date(b.end_at).getTime() - new Date(b.start_at).getTime();
|
|
250
|
-
return bDuration - aDuration; // Longest first
|
|
251
|
-
});
|
|
252
|
-
orderedEventIds.push(...colEvents.map((e) => e.id));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// For each event in the group, store the ordered group and column number
|
|
256
|
-
sortedGroup.forEach((event) => {
|
|
257
|
-
eventOverlapCounts.set(event.id, sortedGroup.length);
|
|
258
|
-
eventOverlapGroups.set(event.id, orderedEventIds);
|
|
259
|
-
eventColumns.set(event.id, groupEventColumns.get(event.id) || 0);
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Assign levels (for fallback positioning) using the original algorithm
|
|
264
|
-
sortedDayEvents.forEach((event) => {
|
|
265
|
-
const eventStart = new Date(event.start_at).getTime();
|
|
266
|
-
|
|
267
|
-
// Find the first level where this event can fit
|
|
268
|
-
let level = 0;
|
|
269
|
-
while (level < MAX_LEVEL) {
|
|
270
|
-
if (
|
|
271
|
-
!levelEndTimes[level] ||
|
|
272
|
-
eventStart >= (levelEndTimes?.[level] ?? 0)
|
|
273
|
-
) {
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
level++;
|
|
214
|
+
const allEvents = useMemo(() => {
|
|
215
|
+
const realEventIds = new Set(filteredRealEvents.map((event) => event.id));
|
|
216
|
+
const filteredPreviewEvents = previewEvents.filter(
|
|
217
|
+
(event) => !realEventIds.has(event.id)
|
|
218
|
+
);
|
|
219
|
+
return [...filteredRealEvents, ...filteredPreviewEvents];
|
|
220
|
+
}, [filteredRealEvents, previewEvents]);
|
|
221
|
+
|
|
222
|
+
const visibleEvents = useMemo(() => {
|
|
223
|
+
const nextVisibleEvents: CalendarEvent[] = [];
|
|
224
|
+
|
|
225
|
+
for (const event of allEvents) {
|
|
226
|
+
for (const processedEvent of processCalendarEvent(event, tz)) {
|
|
227
|
+
if (visibleDayKeys.has(getDayKeyFromIso(processedEvent.start_at, tz))) {
|
|
228
|
+
nextVisibleEvents.push(processedEvent);
|
|
277
229
|
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
278
232
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
// Store the level for this event
|
|
283
|
-
eventLevels.set(event.id, level);
|
|
233
|
+
return nextVisibleEvents;
|
|
234
|
+
}, [allEvents, tz, visibleDayKeys]);
|
|
284
235
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// Return events with assigned levels and overlap information
|
|
291
|
-
return sortedEvents.map((event) => ({
|
|
292
|
-
...event,
|
|
293
|
-
_level: eventLevels.get(event.id) || 0,
|
|
294
|
-
_overlapCount: eventOverlapCounts.get(event.id) || 1,
|
|
295
|
-
_overlapGroup: eventOverlapGroups.get(event.id) || [event.id],
|
|
296
|
-
_column: eventColumns.get(event.id) || 0,
|
|
297
|
-
}));
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// Get events with levels assigned
|
|
301
|
-
const eventsWithLevels = assignLevels();
|
|
236
|
+
const eventsWithLevels = useMemo(
|
|
237
|
+
() => assignEventLayout(visibleEvents, tz),
|
|
238
|
+
[visibleEvents, tz]
|
|
239
|
+
);
|
|
302
240
|
|
|
303
241
|
const columns = dates.length;
|
|
304
242
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
5
|
+
import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
|
|
6
|
+
import dayjs from 'dayjs';
|
|
7
|
+
import timezone from 'dayjs/plugin/timezone';
|
|
8
|
+
import utc from 'dayjs/plugin/utc';
|
|
9
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { EventCard } from './event-card';
|
|
11
|
+
|
|
12
|
+
dayjs.extend(utc);
|
|
13
|
+
dayjs.extend(timezone);
|
|
14
|
+
|
|
15
|
+
class ResizeObserverMock {
|
|
16
|
+
disconnect = vi.fn();
|
|
17
|
+
observe = vi.fn();
|
|
18
|
+
unobserve = vi.fn();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const calendarMocks = vi.hoisted(() => ({
|
|
22
|
+
deleteEvent: vi.fn(),
|
|
23
|
+
hideModal: vi.fn(),
|
|
24
|
+
isEventReadOnly: vi.fn(() => true),
|
|
25
|
+
openModal: vi.fn(),
|
|
26
|
+
setHoveredBaseEventId: vi.fn(),
|
|
27
|
+
setHoveredEventColumn: vi.fn(),
|
|
28
|
+
updateEvent: vi.fn(),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock('@tuturuuu/ui/hooks/use-calendar', () => ({
|
|
32
|
+
useCalendar: () => ({
|
|
33
|
+
affectedEventIds: new Set<string>(),
|
|
34
|
+
deleteEvent: calendarMocks.deleteEvent,
|
|
35
|
+
disableBuiltInEventUi: true,
|
|
36
|
+
hideModal: calendarMocks.hideModal,
|
|
37
|
+
hoveredBaseEventId: null,
|
|
38
|
+
hoveredEventColumn: null,
|
|
39
|
+
isEventReadOnly: calendarMocks.isEventReadOnly,
|
|
40
|
+
openModal: calendarMocks.openModal,
|
|
41
|
+
preservePastEventOpacity: true,
|
|
42
|
+
readOnly: false,
|
|
43
|
+
renderEventContextMenu: undefined,
|
|
44
|
+
setHoveredBaseEventId: calendarMocks.setHoveredBaseEventId,
|
|
45
|
+
setHoveredEventColumn: calendarMocks.setHoveredEventColumn,
|
|
46
|
+
updateEvent: calendarMocks.updateEvent,
|
|
47
|
+
}),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
vi.mock('./settings/settings-context', () => ({
|
|
51
|
+
useCalendarSettings: () => ({
|
|
52
|
+
settings: {
|
|
53
|
+
appearance: { timeFormat: '24h' },
|
|
54
|
+
timezone: { timezone: 'Asia/Ho_Chi_Minh' },
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
function renderEventCard(event: CalendarEvent) {
|
|
60
|
+
const queryClient = new QueryClient({
|
|
61
|
+
defaultOptions: {
|
|
62
|
+
mutations: { retry: false },
|
|
63
|
+
queries: { retry: false },
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return render(
|
|
68
|
+
<QueryClientProvider client={queryClient}>
|
|
69
|
+
<div className="calendar-cell" style={{ height: 1920, width: 240 }}>
|
|
70
|
+
<EventCard
|
|
71
|
+
dates={[new Date('2026-06-26T00:00:00.000Z')]}
|
|
72
|
+
event={event}
|
|
73
|
+
wsId="workspace-1"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
</QueryClientProvider>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('EventCard read-only adapter events', () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
vi.clearAllMocks();
|
|
83
|
+
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
|
84
|
+
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
|
85
|
+
callback(0);
|
|
86
|
+
return 0;
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('opens read-only events but hides resize controls', () => {
|
|
91
|
+
const { container } = renderEventCard({
|
|
92
|
+
id: 'event-1',
|
|
93
|
+
title: 'Read only session',
|
|
94
|
+
start_at: '2026-06-26T08:30:00.000Z',
|
|
95
|
+
end_at: '2026-06-26T09:30:00.000Z',
|
|
96
|
+
ws_id: 'workspace-1',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(calendarMocks.isEventReadOnly).toHaveBeenCalledWith(
|
|
100
|
+
expect.objectContaining({ id: 'event-1' })
|
|
101
|
+
);
|
|
102
|
+
expect(container.querySelector('.cursor-s-resize')).toBeNull();
|
|
103
|
+
|
|
104
|
+
fireEvent.click(screen.getByRole('button', { name: /read only session/i }));
|
|
105
|
+
|
|
106
|
+
expect(calendarMocks.openModal).toHaveBeenCalledWith('event-1');
|
|
107
|
+
expect(calendarMocks.updateEvent).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('shows a Google Calendar provider icon before synced event titles', () => {
|
|
111
|
+
renderEventCard({
|
|
112
|
+
id: 'event-google',
|
|
113
|
+
title: 'Google sync',
|
|
114
|
+
start_at: '2026-06-26T08:30:00.000Z',
|
|
115
|
+
end_at: '2026-06-26T09:30:00.000Z',
|
|
116
|
+
google_event_id: 'google-event-1',
|
|
117
|
+
provider: 'google',
|
|
118
|
+
ws_id: 'workspace-1',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(screen.getByTestId('google-calendar-logo')).toBeInTheDocument();
|
|
122
|
+
expect(
|
|
123
|
+
screen.queryByTestId('microsoft-outlook-logo')
|
|
124
|
+
).not.toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('shows a Microsoft Outlook provider icon before Microsoft synced event titles', () => {
|
|
128
|
+
renderEventCard({
|
|
129
|
+
id: 'event-microsoft',
|
|
130
|
+
title: 'Outlook sync',
|
|
131
|
+
start_at: '2026-06-26T08:30:00.000Z',
|
|
132
|
+
end_at: '2026-06-26T09:30:00.000Z',
|
|
133
|
+
external_event_id: 'outlook-event-1',
|
|
134
|
+
provider: 'microsoft',
|
|
135
|
+
ws_id: 'workspace-1',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(screen.getByTestId('microsoft-outlook-logo')).toBeInTheDocument();
|
|
139
|
+
expect(
|
|
140
|
+
screen.queryByTestId('google-calendar-logo')
|
|
141
|
+
).not.toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('does not show provider icons for local calendar events', () => {
|
|
145
|
+
renderEventCard({
|
|
146
|
+
id: 'event-local',
|
|
147
|
+
title: 'Local event',
|
|
148
|
+
start_at: '2026-06-26T08:30:00.000Z',
|
|
149
|
+
end_at: '2026-06-26T09:30:00.000Z',
|
|
150
|
+
provider: 'tuturuuu',
|
|
151
|
+
ws_id: 'workspace-1',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(
|
|
155
|
+
screen.queryByTestId('google-calendar-logo')
|
|
156
|
+
).not.toBeInTheDocument();
|
|
157
|
+
expect(
|
|
158
|
+
screen.queryByTestId('microsoft-outlook-logo')
|
|
159
|
+
).not.toBeInTheDocument();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('renders updating events as a subtle dashed pending card', () => {
|
|
163
|
+
renderEventCard({
|
|
164
|
+
id: 'event-updating',
|
|
165
|
+
title: 'Updating event',
|
|
166
|
+
start_at: '2026-06-26T08:30:00.000Z',
|
|
167
|
+
end_at: '2026-06-26T09:30:00.000Z',
|
|
168
|
+
ws_id: 'workspace-1',
|
|
169
|
+
_optimisticStatus: 'updating',
|
|
170
|
+
} as CalendarEvent & { _optimisticStatus: 'updating' });
|
|
171
|
+
|
|
172
|
+
expect(screen.getByTestId('calendar-event-event-updating')).toHaveClass(
|
|
173
|
+
'outline-dashed',
|
|
174
|
+
'opacity-60'
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
});
|