@allbluecn/web-app 0.18.2 → 0.19.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/package.json +66 -51
- package/src/__tests__/components/auth/auth-layout.test.tsx +13 -1
- package/src/__tests__/{knowledge-public → knowledge/public}/canonical-id.test.ts +1 -1
- package/src/__tests__/{knowledge-public → knowledge/public}/favicon.test.ts +1 -1
- package/src/__tests__/{knowledge-public → knowledge/public}/github-sync.test.ts +1 -1
- package/src/__tests__/{knowledge-public → knowledge/public}/share-merge.test.ts +7 -7
- package/src/__tests__/security/xss-sanitize.test.ts +9 -8
- package/src/components/ai/__tests__/chat-thread-id-stability.test.tsx +125 -0
- package/src/components/ai/ai-chat-prompt.tsx +8 -2
- package/src/components/ai/ai-prompt-input/index.tsx +2 -1
- package/src/components/ai/global-harness-sidebar/__tests__/global-ai-prompts.test.ts +1 -2
- package/src/components/ai/global-harness-sidebar/index.tsx +22 -10
- package/src/components/bug/__tests__/board-subgroups.test.tsx +512 -0
- package/src/components/bug/__tests__/board-view.test.tsx +350 -0
- package/src/components/bug/__tests__/bug-list-loading-defer.test.tsx +85 -0
- package/src/components/bug/__tests__/bug-toolbar.test.tsx +7 -1
- package/src/components/bug/__tests__/comment-reactions.test.tsx +182 -0
- package/src/components/bug/__tests__/date-and-lightbox.test.tsx +96 -0
- package/src/components/bug/__tests__/export-entry.test.tsx +217 -0
- package/src/components/bug/__tests__/filters-compat.test.tsx +157 -0
- package/src/components/bug/__tests__/fix-request-race.test.tsx +396 -0
- package/src/components/bug/__tests__/peek-modes.test.tsx +365 -0
- package/src/components/bug/__tests__/saved-views.test.tsx +309 -0
- package/src/components/bug/__tests__/story-bugs-panel.test.tsx +146 -0
- package/src/components/bug/__tests__/subgroup-header.test.tsx +150 -0
- package/src/components/bug/__tests__/toolbar-shell.test.tsx +369 -0
- package/src/components/bug/__tests__/use-paged-bug-groups.test.tsx +58 -0
- package/src/components/bug/__tests__/view-type.test.tsx +247 -0
- package/src/components/bug/bug-list-view.tsx +310 -13
- package/src/components/bug/bug-panel-header.tsx +84 -8
- package/src/components/bug/bug-toolbar.tsx +268 -116
- package/src/components/bug/create-bug-dialog.tsx +1 -1
- package/src/components/bug/detail/__tests__/activity-timeline.test.tsx +1 -0
- package/src/components/bug/detail/__tests__/bug-detail-collab.test.tsx +236 -0
- package/src/components/bug/detail/__tests__/repro-report-card.test.tsx +2 -1
- package/src/components/bug/detail/activity-timeline.tsx +1 -1
- package/src/components/bug/detail/bug-detail-body.tsx +316 -18
- package/src/components/bug/detail/comment-item.tsx +7 -2
- package/src/components/bug/detail/comment-reactions.tsx +138 -0
- package/src/components/bug/detail/error-context.tsx +29 -9
- package/src/components/bug/detail/props-section.tsx +31 -3
- package/src/components/bug/detail/repro-report-card.tsx +1 -1
- package/src/components/bug/display/constants.ts +4 -2
- package/src/components/bug/display/display-dropdown.tsx +76 -280
- package/src/components/bug/display/types.ts +24 -8
- package/src/components/bug/display/use-paged-bug-groups.ts +183 -32
- package/src/components/bug/filters/{filter-value-input.tsx → field-configs.tsx} +218 -106
- package/src/components/bug/filters/index.tsx +14 -5
- package/src/components/bug/inline-editors/assignee-editor.tsx +2 -2
- package/src/components/bug/inline-editors/labels-editor.tsx +1 -1
- package/src/components/bug/inline-editors/severity-editor.tsx +37 -18
- package/src/components/bug/inline-editors/state-editor.tsx +1 -1
- package/src/components/bug/peek-panel.tsx +153 -35
- package/src/components/bug/select-bug-dialog.tsx +500 -0
- package/src/components/bug/story-bugs-panel.tsx +1090 -0
- package/src/components/bug/types.ts +9 -2
- package/src/components/bug/views/board-view.tsx +661 -0
- package/src/components/bug/views/bug-list/batch-toolbar.tsx +25 -34
- package/src/components/bug/views/bug-list/group.tsx +28 -13
- package/src/components/bug/views/bug-list/index.tsx +223 -59
- package/src/components/bug/views/bug-list/row.tsx +7 -1
- package/src/components/bug/views/create-view-dialog.tsx +160 -0
- package/src/components/bug/views/view-list-section.tsx +140 -0
- package/src/components/bug/views/view-selector-button.tsx +145 -0
- package/src/components/bug/views/view-switcher.tsx +8 -6
- package/src/components/bug/views/view-types.ts +162 -0
- package/src/components/collab/__tests__/activity-attribution.test.tsx +211 -0
- package/src/components/collab/__tests__/canvas-collab-panel.test.tsx +118 -0
- package/src/components/collab/__tests__/collab-presence-header.test.tsx +335 -0
- package/src/components/collab/__tests__/entity-presence-badge.test.tsx +226 -0
- package/src/components/collab/activity-attribution.tsx +221 -0
- package/src/components/collab/canvas-collab-panel.tsx +104 -0
- package/src/components/collab/collab-presence-header.tsx +220 -0
- package/src/components/collab/entity-presence-badge.tsx +130 -0
- package/src/components/collab/field-editing-pill.tsx +137 -0
- package/src/components/command/command-palette.tsx +6 -6
- package/src/components/design/canvas/document/__tests__/canvas-tree-fixture-snapshot.ts +589 -0
- package/src/components/design/canvas/document/__tests__/canvas-tree-reconcile.test.ts +159 -0
- package/src/components/design/canvas/panels/toolbar.tsx +3 -2
- package/src/components/design/canvas/plugins/presence/__tests__/canvas-collab-wiring.test.tsx +660 -0
- package/src/components/design/canvas/plugins/presence/__tests__/layout.test.ts +292 -0
- package/src/components/design/canvas/plugins/presence/__tests__/presence-overlay.test.tsx +139 -0
- package/src/components/design/canvas/plugins/presence/layout.ts +222 -0
- package/src/components/design/canvas/plugins/presence/presence-layer.tsx +353 -0
- package/src/components/design/canvas/plugins/presence/presence-overlay.tsx +155 -0
- package/src/components/design/canvas/plugins/presence/viewer-badge.tsx +80 -0
- package/src/components/design/canvas/react/__tests__/hooks.test.tsx +4 -4
- package/src/components/design/editor/collaboration/__tests__/design-agent-panel-regenerate.test.tsx +3 -1
- package/src/components/design/editor/collaboration/__tests__/design-agent-panel-stream.test.tsx +3 -1
- package/src/components/design/editor/collaboration/design-agent-panel.tsx +3 -0
- package/src/components/design/editor/collaboration/design-comment-review-panel.tsx +5 -5
- package/src/components/design/editor/dashboard/create-design-dialog.tsx +1 -1
- package/src/components/design/editor/dashboard/design-dashboard.tsx +1 -10
- package/src/components/design/editor/layout/__tests__/collab-param-wiring.test.tsx +153 -0
- package/src/components/design/editor/layout/__tests__/no-page-save-toast.test.tsx +8 -0
- package/src/components/design/editor/layout/design-editor-layout.tsx +28 -1
- package/src/components/design/editor/layout/design-navigation.tsx +0 -4
- package/src/components/design/editor/layout/hooks/__tests__/use-design-auto-save-collab.test.tsx +130 -0
- package/src/components/design/editor/layout/hooks/use-design-auto-save.ts +18 -1
- package/src/components/design/editor/share/share-comment-panel.tsx +37 -20
- package/src/components/design/editor/share/share-layout.tsx +3 -0
- package/src/components/design/editor/share/share-nav-bar.tsx +7 -1
- package/src/components/design/editor/share/share-theme-toggle.tsx +10 -15
- package/src/components/design/editor/tiptap/node/paragraph-node/paragraph-node.scss +1 -1
- package/src/components/harness/__tests__/composer-draft.test.ts +126 -0
- package/src/components/harness/composer/composer-editor.tsx +64 -6
- package/src/components/harness/composer/harness-composer.tsx +1 -1
- package/src/components/harness/harness-layout.tsx +1 -1
- package/src/components/harness/harness-workbench-page.tsx +6 -1
- package/src/components/harness/turn-group.tsx +1 -1
- package/src/components/hotkeys/shortcut-badge.tsx +4 -4
- package/src/components/knowledge/dialog/knowledge-form-dialog.tsx +1 -1
- package/src/components/knowledge/editor/__tests__/knowledge-doc-editor-collab.test.tsx +542 -0
- package/src/components/knowledge/editor/doc-header.tsx +7 -49
- package/src/components/knowledge/editor/knowledge-doc-editor.tsx +264 -51
- package/src/components/knowledge/editor/mindmap-doc-editor.tsx +3 -19
- package/src/components/knowledge/knowledge-homepage.tsx +3 -3
- package/src/components/knowledge/knowledge-toolbar.tsx +8 -8
- package/src/components/{knowledge-public → knowledge/public}/bookmark-form-dialog.tsx +4 -4
- package/src/components/{knowledge-public → knowledge/public}/knowledge-card.tsx +1 -15
- package/src/components/{knowledge-public → knowledge/public}/knowledge-form-dialog.tsx +9 -9
- package/src/components/{knowledge-public → knowledge/public}/legal-compliance-editor.tsx +35 -28
- package/src/components/{knowledge-public → knowledge/public}/sharers-avatar-group.tsx +3 -2
- package/src/components/layout/layout-responsive-provider.tsx +79 -0
- package/src/components/layout/sidebar-03/__tests__/presence-avatar-stack.test.tsx +148 -0
- package/src/components/layout/sidebar-03/app-sidebar.tsx +9 -3
- package/src/components/layout/sidebar-03/index.tsx +19 -4
- package/src/components/layout/sidebar-03/nav-main.tsx +28 -10
- package/src/components/layout/sidebar-03/presence-avatar-stack.tsx +112 -0
- package/src/components/layout/sidebar-03/user-menu.tsx +3 -3
- package/src/components/project/create-project-dialog.tsx +1 -1
- package/src/components/project/invite-member-dialog.tsx +1 -1
- package/src/components/project/project-activity-list.tsx +2 -2
- package/src/components/project/project-member-manager.tsx +2 -2
- package/src/components/reui/__tests__/kanban-drop-indicator.test.ts +88 -0
- package/src/components/reui/__tests__/kanban-keyboard.test.ts +262 -0
- package/src/components/reui/event-calendar/event-calendar-agenda-view.tsx +236 -0
- package/src/components/reui/event-calendar/event-calendar-content.tsx +80 -0
- package/src/components/reui/event-calendar/event-calendar-dnd.tsx +1062 -0
- package/src/components/reui/event-calendar/event-calendar-event.tsx +555 -0
- package/src/components/reui/event-calendar/event-calendar-i18n.tsx +291 -0
- package/src/components/reui/event-calendar/event-calendar-lib.tsx +677 -0
- package/src/components/reui/event-calendar/event-calendar-month-view.tsx +1287 -0
- package/src/components/reui/event-calendar/event-calendar-nav.tsx +615 -0
- package/src/components/reui/event-calendar/event-calendar-recurrence.tsx +592 -0
- package/src/components/reui/event-calendar/event-calendar-resource-view.tsx +810 -0
- package/src/components/reui/event-calendar/event-calendar-time-grid.tsx +1366 -0
- package/src/components/reui/event-calendar/event-calendar-types.tsx +240 -0
- package/src/components/reui/event-calendar/event-calendar.tsx +1916 -0
- package/src/components/reui/gantt/__tests__/gantt-row-window.test.ts +243 -0
- package/src/components/reui/gantt/__tests__/gantt-view-virtualize.test.tsx +243 -0
- package/src/components/reui/gantt/gantt-bar.tsx +439 -0
- package/src/components/reui/gantt/gantt-dnd.tsx +1000 -0
- package/src/components/reui/gantt/gantt-i18n.tsx +287 -0
- package/src/components/reui/gantt/gantt-lib.tsx +695 -0
- package/src/components/reui/gantt/gantt-nav.tsx +446 -0
- package/src/components/reui/gantt/gantt-recurrence.tsx +402 -0
- package/src/components/reui/gantt/gantt-row-window.ts +119 -0
- package/src/components/reui/gantt/gantt-types.tsx +283 -0
- package/src/components/reui/gantt/gantt-view.tsx +4466 -0
- package/src/components/reui/gantt/gantt.tsx +1822 -0
- package/src/components/reui/gantt/use-gantt-row-window.ts +159 -0
- package/src/components/reui/icon-stack.tsx +90 -0
- package/src/components/reui/kanban-keyboard.ts +268 -0
- package/src/components/reui/kanban.tsx +1227 -0
- package/src/components/settings/personal-team/account/account-client.tsx +3 -2
- package/src/components/settings/personal-team/team/invitation-cards/invitation-history-list.tsx +5 -5
- package/src/components/settings/personal-team/team/team-client.tsx +2 -1
- package/src/components/settings/personal-team/team/team-member-list.tsx +4 -4
- package/src/components/share/share-presence-bar.tsx +182 -0
- package/src/components/shared-kit/auth/auth-client.ts +5 -0
- package/src/components/shared-kit/avatars/__tests__/avatar-shape.test.ts +44 -0
- package/src/components/shared-kit/avatars/__tests__/user-avatar.test.tsx +46 -0
- package/src/components/{tags/tags-collection.ts → shared-kit/avatars/avatar-shape.ts} +14 -20
- package/src/components/shared-kit/avatars/notion-avatar.tsx +62 -11
- package/src/components/shared-kit/avatars/user-avatar.tsx +7 -4
- package/src/components/shared-kit/list-kit/__tests__/batch-toolbar-shell.test.tsx +133 -0
- package/src/components/shared-kit/list-kit/__tests__/frozen-header.test.ts +89 -0
- package/src/components/shared-kit/list-kit/__tests__/level-badge.test.tsx +85 -0
- package/src/components/shared-kit/list-kit/__tests__/toolbar-shell.test.tsx +178 -0
- package/src/components/shared-kit/list-kit/batch-toolbar-shell.tsx +111 -0
- package/src/components/shared-kit/list-kit/csv-download.ts +46 -0
- package/src/components/shared-kit/list-kit/display/__tests__/dropdown.test.tsx +215 -0
- package/src/components/shared-kit/list-kit/display/dropdown.tsx +582 -0
- package/src/components/shared-kit/list-kit/display/types.ts +64 -0
- package/src/components/shared-kit/list-kit/display/values.ts +60 -0
- package/src/components/shared-kit/list-kit/filters/__tests__/create-filters-module.test.tsx +181 -0
- package/src/components/shared-kit/list-kit/filters/__tests__/types.test.ts +133 -0
- package/src/components/shared-kit/list-kit/filters/index.tsx +757 -0
- package/src/components/shared-kit/list-kit/filters/types.ts +68 -0
- package/src/components/shared-kit/list-kit/frozen-header.ts +45 -0
- package/src/components/shared-kit/list-kit/icon-semantics.ts +50 -0
- package/src/components/shared-kit/list-kit/kanban/__tests__/drop-layout.test.ts +139 -0
- package/src/components/shared-kit/list-kit/kanban/__tests__/frozen-header.test.ts +111 -0
- package/src/components/shared-kit/list-kit/kanban/__tests__/instance-key.test.ts +130 -0
- package/src/components/shared-kit/list-kit/kanban/__tests__/kanban-board-view.test.tsx +901 -0
- package/src/components/shared-kit/list-kit/kanban/__tests__/manual-order.test.ts +35 -0
- package/src/components/shared-kit/list-kit/kanban/drop-commit.ts +212 -0
- package/src/components/shared-kit/list-kit/kanban/drop-layout.ts +98 -0
- package/src/components/shared-kit/list-kit/kanban/frozen-header.ts +88 -0
- package/src/components/shared-kit/list-kit/kanban/instance-key.ts +83 -0
- package/src/components/shared-kit/list-kit/kanban/kanban-board-view.tsx +1414 -0
- package/src/components/shared-kit/list-kit/kanban/manual-order.ts +39 -0
- package/src/components/shared-kit/list-kit/level-badge.tsx +71 -0
- package/src/components/shared-kit/list-kit/toolbar-shell.tsx +103 -0
- package/src/components/shared-kit/loading/__tests__/use-deferred-flag.test.tsx +115 -0
- package/src/components/shared-kit/loading/use-deferred-flag.ts +35 -0
- package/src/components/shared-kit/peek/dock-slot.ts +62 -0
- package/src/components/shared-kit/peek/peek-mode.ts +34 -0
- package/src/components/shared-kit/theme/animated-theme-toggler.tsx +13 -7
- package/src/components/shared-kit/theme/theme-toggle.tsx +7 -18
- package/src/components/shared-kit/views/view-icon.tsx +49 -0
- package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +215 -0
- package/src/components/story/__tests__/story-list-loading-defer.test.tsx +88 -0
- package/src/components/story/__tests__/story-list-view-peek-sync.test.tsx +242 -0
- package/src/components/story/__tests__/story-list-view-restore.test.tsx +45 -3
- package/src/components/story/__tests__/story-list-view-ssr.test.tsx +0 -1
- package/src/components/story/ai/__tests__/action-registry.test.ts +11 -11
- package/src/components/story/ai/action-registry.ts +5 -7
- package/src/components/story/ai/handlers/stream-text.ts +1 -3
- package/src/components/story/ai/story-ai-toolbar.tsx +58 -16
- package/src/components/story/ai/use-toolbar-overflow.ts +4 -7
- package/src/components/story/columns.tsx +2 -2
- package/src/components/story/constants.ts +17 -1
- package/src/components/story/create-story-dialog.tsx +8 -7
- package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +38 -4
- package/src/components/story/detail/__tests__/categories.test.tsx +52 -26
- package/src/components/story/detail/__tests__/story-collab.test.tsx +897 -0
- package/src/components/story/detail/activity-tabs.tsx +5 -4
- package/src/components/story/detail/activity-timeline.tsx +15 -3
- package/src/components/story/detail/comment-input.tsx +2 -2
- package/src/components/story/detail/comment-item.tsx +6 -3
- package/src/components/story/detail/comment-thread.tsx +1 -0
- package/src/components/story/detail/reaction-bar.tsx +1 -1
- package/src/components/story/detail/story-detail-body.tsx +501 -76
- package/src/components/story/detail/story-detail-toolbar/__tests__/related-test-cases.test.tsx +173 -10
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +144 -8
- package/src/components/story/detail/story-detail-toolbar/categories.tsx +82 -38
- package/src/components/story/detail/story-detail-toolbar/children.tsx +254 -174
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +140 -58
- package/src/components/story/detail/story-detail-toolbar/index.tsx +613 -0
- package/src/components/story/detail/story-detail-toolbar/related-test-cases.tsx +1010 -87
- package/src/components/story/detail/story-detail-toolbar/scroll-hint.tsx +57 -0
- package/src/components/story/detail/story-detail-toolbar/use-panel-frozen-groups.ts +97 -0
- package/src/components/story/detail/story-properties.tsx +9 -5
- package/src/components/story/display/__tests__/display-dropdown-helpers.test.ts +17 -1
- package/src/components/story/display/__tests__/display-dropdown.test.tsx +9 -0
- package/src/components/story/display/__tests__/flatten-story-groups.test.ts +42 -0
- package/src/components/story/display/__tests__/group-fields.test.ts +81 -1
- package/src/components/story/display/__tests__/group-resolver.test.ts +153 -1
- package/src/components/story/display/__tests__/use-collapsed-subgroups.test.ts +158 -0
- package/src/components/story/display/__tests__/use-display-options.test.ts +32 -3
- package/src/components/story/display/__tests__/use-fit-count.test.ts +198 -0
- package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +77 -3
- package/src/components/story/display/constants.ts +23 -2
- package/src/components/story/display/display-dropdown.tsx +95 -397
- package/src/components/story/display/flatten-story-groups.ts +39 -0
- package/src/components/story/display/group-fields.ts +32 -1
- package/src/components/story/display/group-resolver.tsx +128 -5
- package/src/components/story/display/types.ts +23 -42
- package/src/components/story/display/use-collapsed-subgroups.ts +109 -0
- package/src/components/story/display/use-display-options.ts +21 -5
- package/src/components/story/display/use-fit-count.ts +124 -0
- package/src/components/story/display/use-paged-story-groups.ts +174 -31
- package/src/components/story/filters/field-configs.tsx +263 -0
- package/src/components/story/filters/index.tsx +14 -5
- package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +2 -0
- package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +15 -0
- package/src/components/story/inline-editors/assignee-editor.tsx +7 -5
- package/src/components/story/inline-editors/date-editor.tsx +7 -2
- package/src/components/story/inline-editors/inline-trigger.tsx +17 -4
- package/src/components/story/inline-editors/labels-editor.tsx +243 -35
- package/src/components/story/inline-editors/parent-story-editor.tsx +1 -1
- package/src/components/story/inline-editors/priority-editor.tsx +6 -4
- package/src/components/story/inline-editors/state-editor.tsx +6 -4
- package/src/components/story/inline-editors/story-select-dialog.tsx +56 -19
- package/src/components/story/peek-panel.tsx +234 -51
- package/src/components/story/relations/__tests__/design-select-dialog.test.tsx +131 -0
- package/src/components/story/relations/attachment-manage-dialog.tsx +1 -1
- package/src/components/story/relations/design-select-dialog.tsx +285 -0
- package/src/components/story/relations/knowledge-select-dialog.tsx +3 -3
- package/src/components/story/relations/link-manage-dialog.tsx +2 -2
- package/src/components/story/story-list-view.tsx +175 -17
- package/src/components/story/story-panel-header.tsx +18 -11
- package/src/components/story/story-toolbar.tsx +177 -126
- package/src/components/story/transfer-story-popover.tsx +1 -1
- package/src/components/story/types.ts +11 -2
- package/src/components/story/views/__tests__/calendar-view.test.tsx +262 -0
- package/src/components/story/views/__tests__/kanban-drop-subgroup.test.ts +68 -0
- package/src/components/story/views/__tests__/kanban-peek-sync.test.tsx +184 -0
- package/src/components/story/views/__tests__/kanban-view-membership-sync.test.tsx +179 -0
- package/src/components/story/views/__tests__/timeline-view.test.tsx +453 -0
- package/src/components/story/views/__tests__/view-icon.test.tsx +45 -0
- package/src/components/story/views/__tests__/view-types.test.ts +0 -8
- package/src/components/story/views/calendar/__tests__/mappers.test.ts +201 -0
- package/src/components/story/views/calendar/__tests__/slot-draft.test.ts +78 -0
- package/src/components/story/views/calendar/drag-blocked.ts +41 -0
- package/src/components/story/views/calendar/i18n.ts +91 -0
- package/src/components/story/views/calendar/mappers.ts +119 -0
- package/src/components/story/views/calendar/slot-draft.ts +48 -0
- package/src/components/story/views/calendar-view.tsx +490 -223
- package/src/components/story/views/create-view-dialog.tsx +6 -6
- package/src/components/story/views/kanban-view.tsx +618 -0
- package/src/components/story/views/{__tests__ → story-list/__tests__}/use-flat-rows.test.ts +1 -1
- package/src/components/story/views/story-list/batch-preview-dialog.tsx +3 -3
- package/src/components/story/views/story-list/batch-toolbar.tsx +17 -34
- package/src/components/story/views/story-list/group.tsx +4 -2
- package/src/components/story/views/story-list/index.tsx +142 -34
- package/src/components/story/views/story-list/row.tsx +4 -0
- package/src/components/story/views/{use-flat-rows.ts → story-list/use-flat-rows.ts} +2 -2
- package/src/components/story/views/timeline/__tests__/mappers.test.ts +742 -0
- package/src/components/story/views/timeline/__tests__/sentinel-geometry.test.ts +76 -0
- package/src/components/story/views/timeline/__tests__/use-date-commit.test.tsx +372 -0
- package/src/components/story/views/timeline/i18n.ts +96 -0
- package/src/components/story/views/timeline/mappers.ts +506 -0
- package/src/components/story/views/timeline/sentinel-geometry.ts +43 -0
- package/src/components/story/views/timeline/use-date-commit.ts +169 -0
- package/src/components/story/views/timeline-view.tsx +593 -222
- package/src/components/story/views/view-list-section.tsx +4 -4
- package/src/components/story/views/view-selector-button.tsx +9 -10
- package/src/components/story/views/view-switcher.tsx +7 -1
- package/src/components/story/views/view-types.ts +1 -5
- package/src/components/testcase/__tests__/batch-toolbar.test.tsx +245 -0
- package/src/components/testcase/__tests__/board-view.test.tsx +241 -0
- package/src/components/testcase/__tests__/case-row.test.tsx +24 -10
- package/src/components/testcase/__tests__/constants.test.ts +151 -0
- package/src/components/testcase/__tests__/create-dialog-scope.test.tsx +173 -0
- package/src/components/testcase/__tests__/detail-properties.test.tsx +19 -0
- package/src/components/testcase/__tests__/display-options.test.tsx +154 -0
- package/src/components/testcase/__tests__/export-entry.test.tsx +279 -0
- package/src/components/testcase/__tests__/filters.test.tsx +280 -0
- package/src/components/testcase/__tests__/list-view-scope.test.tsx +290 -0
- package/src/components/testcase/__tests__/list-view.test.tsx +552 -7
- package/src/components/testcase/__tests__/manual-move.test.ts +69 -0
- package/src/components/testcase/__tests__/peek-panel.test.tsx +89 -8
- package/src/components/testcase/__tests__/toolbar.test.tsx +302 -0
- package/src/components/testcase/__tests__/view-fallback.test.tsx +272 -0
- package/src/components/testcase/__tests__/view-management.test.tsx +179 -0
- package/src/components/testcase/ai/generate-dialog.tsx +1 -1
- package/src/components/testcase/constants.ts +83 -0
- package/src/components/testcase/create-testcase-dialog.tsx +76 -8
- package/src/components/testcase/detail/__tests__/select-cases-dialog.test.tsx +163 -0
- package/src/components/testcase/detail/case-detail-body.tsx +311 -30
- package/src/components/testcase/detail/select-cases-dialog.tsx +465 -0
- package/src/components/testcase/detail/story-link-editor.tsx +1 -1
- package/src/components/testcase/display/display-dropdown.tsx +147 -0
- package/src/components/testcase/display/field-configs.ts +80 -0
- package/src/components/testcase/display/use-testcase-display-options.ts +59 -0
- package/src/components/testcase/filters/field-configs.tsx +748 -0
- package/src/components/testcase/filters/index.tsx +35 -127
- package/src/components/testcase/inline-editors/assignee-editor.tsx +6 -6
- package/src/components/testcase/inline-editors/folder-editor.tsx +9 -9
- package/src/components/testcase/inline-editors/priority-editor.tsx +47 -31
- package/src/components/testcase/inline-editors/state-editor.tsx +14 -12
- package/src/components/testcase/inline-editors/type-editor.tsx +31 -20
- package/src/components/testcase/list/case-row.tsx +60 -2
- package/src/components/testcase/list/grouped-list.tsx +3 -2
- package/src/components/testcase/list-view.tsx +633 -137
- package/src/components/testcase/peek-panel.tsx +69 -11
- package/src/components/testcase/plans/plan-list.tsx +6 -1
- package/src/components/testcase/step-editor.tsx +34 -3
- package/src/components/testcase/testcase-toolbar.tsx +349 -94
- package/src/components/testcase/views/batch-toolbar.tsx +203 -0
- package/src/components/testcase/views/board-view.tsx +546 -0
- package/src/components/testcase/views/create-view-dialog.tsx +26 -14
- package/src/components/testcase/views/move.ts +56 -0
- package/src/components/testcase/views/view-selector-button.tsx +82 -30
- package/src/components/testcase/views/view-switcher.tsx +12 -5
- package/src/components/testcase/views/view-types.ts +50 -27
- package/src/components/tiptap/mindmap/lib/__tests__/shortcuts.test.ts +5 -4
- package/src/components/tiptap/mindmap/lib/shortcuts.ts +4 -3
- package/src/components/tiptap/node/doc-card-node-view.tsx +2 -1
- package/src/components/tiptap/notion-like/editor-extensions.ts +231 -0
- package/src/components/tiptap/notion-like/notion-like-editor.tsx +29 -156
- package/src/components/tiptap/ui/ai-menu/ai-menu.tsx +6 -1
- package/src/components/tiptap/ui/doc-reference-interaction/doc-reference-interaction.tsx +2 -1
- package/src/components/ui/checkbox.tsx +4 -4
- package/src/components/ui/popover.tsx +1 -1
- package/src/components/workspace/dialogs/edit-workspace-dialog.tsx +1 -1
- package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +1 -1
- package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +1 -1
- package/src/components/workspace/members/workspace-member-item.tsx +2 -2
- package/src/components/workspace/members/workspace-members-manager.tsx +2 -2
- package/src/config/sidebar-routes.tsx +11 -13
- package/src/design-engine/document/serialize-leafer-json.ts +2 -1
- package/src/design-engine/engine/__tests__/engine-split.test.ts +1 -1
- package/src/design-engine/engine/__tests__/engine.test.ts +20 -0
- package/src/design-engine/engine/app.ts +5 -0
- package/src/design-engine/engine/engine-base.ts +17 -0
- package/src/design-engine/engine/engine-doc.ts +32 -0
- package/src/hooks/__tests__/use-commands.test.tsx +17 -5
- package/src/hooks/use-commands.ts +4 -4
- package/src/hooks/use-toolbar-search.ts +7 -2
- package/src/lib/__tests__/datetime-local.test.ts +46 -0
- package/src/lib/__tests__/datetime.test.ts +7 -0
- package/src/lib/__tests__/layout-responsive.test.ts +123 -0
- package/src/lib/__tests__/sidebar-utils.test.ts +4 -4
- package/src/lib/ai/story-ai-stream.ts +1 -1
- package/src/lib/ai/use-selected-model.ts +15 -5
- package/src/lib/changelog/sdk-versions.ts +22 -22
- package/src/lib/collab/__tests__/bindings-fields.test.ts +496 -0
- package/src/lib/collab/__tests__/bindings-steps.test.ts +332 -0
- package/src/lib/collab/__tests__/presence-store.test.ts +414 -0
- package/src/lib/collab/__tests__/provider-handle-contract.test.ts +89 -0
- package/src/lib/collab/__tests__/provider.test.ts +504 -0
- package/src/lib/collab/__tests__/use-presence-list-refresh.test.tsx +309 -0
- package/src/lib/collab/__tests__/use-presence-report.test.ts +356 -0
- package/src/lib/collab/__tests__/use-share-presence.test.ts +555 -0
- package/src/lib/collab/__tests__/user-color.test.ts +53 -0
- package/src/lib/collab/bindings/__tests__/canvas-local.test.ts +583 -0
- package/src/lib/collab/bindings/__tests__/canvas-presence.test.ts +378 -0
- package/src/lib/collab/bindings/__tests__/canvas-remote.test.ts +454 -0
- package/src/lib/collab/bindings/__tests__/canvas-tree-perf.test.ts +134 -0
- package/src/lib/collab/bindings/canvas-local.ts +415 -0
- package/src/lib/collab/bindings/canvas-presence.ts +420 -0
- package/src/lib/collab/bindings/canvas-remote.ts +226 -0
- package/src/lib/collab/bindings/fields.ts +463 -0
- package/src/lib/collab/bindings/steps.ts +332 -0
- package/src/lib/collab/env.ts +33 -0
- package/src/lib/collab/module-map.ts +131 -0
- package/src/lib/collab/presence-store.ts +350 -0
- package/src/lib/collab/provider.ts +378 -0
- package/src/lib/collab/use-collab-doc.ts +100 -0
- package/src/lib/collab/use-presence-list-refresh.ts +142 -0
- package/src/lib/collab/use-presence-report.ts +176 -0
- package/src/lib/collab/use-self-profile-broadcast.ts +113 -0
- package/src/lib/collab/use-share-presence.ts +561 -0
- package/src/lib/collab/user-color.ts +40 -0
- package/src/lib/collections/index.ts +0 -1
- package/src/lib/collections/reference/bookmark-categories-collection.ts +1 -1
- package/src/lib/commands/__tests__/registry.test.ts +2 -5
- package/src/lib/commands/registry.ts +0 -3
- package/src/lib/commands/types.ts +1 -1
- package/src/lib/datetime.ts +20 -3
- package/src/lib/hooks/__tests__/use-synced-state.test.tsx +54 -0
- package/src/lib/hooks/use-story-panel-open.ts +134 -0
- package/src/lib/hooks/use-synced-state.ts +17 -0
- package/src/lib/hotkeys/__tests__/registry.test.ts +1 -1
- package/src/lib/hotkeys/registry.ts +0 -3
- package/src/lib/layout-responsive.ts +140 -0
- package/src/lib/pacer.ts +10 -1
- package/src/lib/peek-dock.ts +32 -0
- package/src/lib/plugin-runtime/__tests__/plugin-tool-schema-parity.test.ts +140 -0
- package/src/lib/plugin-runtime/plugin-client-tools.ts +28 -46
- package/src/lib/plugin-runtime/plugin-tool-defs.ts +152 -0
- package/src/lib/project-module-meta.ts +1 -8
- package/src/lib/rate-limit.ts +15 -6
- package/src/lib/sanitize.ts +122 -10
- package/src/lib/share-events.ts +20 -11
- package/src/lib/sidebar-utils.ts +1 -1
- package/src/lib/theme/__tests__/theme-mode.test.ts +96 -0
- package/src/lib/theme/theme-mode.ts +77 -0
- package/src/paraglide/messages/_index.js +5312 -1591
- package/src/paraglide/messages/en.js +1880 -685
- package/src/paraglide/messages/zh.js +1877 -682
- package/src/providers/index.tsx +4 -1
- package/src/providers/theme-mode-provider.tsx +135 -0
- package/src/routeTree.gen.ts +78 -101
- package/src/routes/$lang/docs/$/route.lazy.tsx +7 -2
- package/src/routes/-tests/chat-body-validation.test.ts +61 -3
- package/src/routes/__tests__/-edition-routing.test.tsx +12 -8
- package/src/routes/_nav/-components/nav-not-found.tsx +2 -2
- package/src/routes/_nav/bugs/$bugId/route.lazy.tsx +16 -2
- package/src/routes/_nav/bugs/$bugId/route.tsx +11 -2
- package/src/routes/_nav/dashboard/-components/avatar-group.tsx +2 -2
- package/src/routes/_nav/knowledge/$kbId/doc/$docId/route.lazy.tsx +31 -17
- package/src/routes/_nav/knowledge/$kbId/doc/$docId/route.tsx +12 -1
- package/src/routes/_nav/knowledge/$kbId/index.lazy.tsx +4 -2
- package/src/routes/_nav/knowledge/$kbId/route.lazy.tsx +7 -3
- package/src/routes/_nav/knowledge/bookmarks/$id.lazy.tsx +6 -6
- package/src/routes/_nav/knowledge/bookmarks/index.lazy.tsx +10 -10
- package/src/routes/_nav/knowledge/compliance/$id.lazy.tsx +9 -9
- package/src/routes/_nav/knowledge/compliance/edit/$id.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +8 -8
- package/src/routes/_nav/knowledge/dictionaries/$id.lazy.tsx +4 -4
- package/src/routes/_nav/knowledge/dictionaries/index.lazy.tsx +6 -6
- package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge/laws/$id.lazy.tsx +9 -9
- package/src/routes/_nav/knowledge/laws/edit/$id.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge/laws/index.lazy.tsx +11 -11
- package/src/routes/_nav/knowledge/summary/index.lazy.tsx +1 -1
- package/src/routes/_nav/projects/$projectId/index.lazy.tsx +14 -6
- package/src/routes/_nav/route.lazy.tsx +11 -6
- package/src/routes/_nav/settings/account/route.tsx +3 -2
- package/src/routes/_nav/stories/$storyId/route.lazy.tsx +52 -8
- package/src/routes/_nav/stories/$storyId/route.tsx +11 -0
- package/src/routes/_nav/stories/-components/story-detail-skeleton.tsx +1 -1
- package/src/{components/tags/pages/tag-detail-page.tsx → routes/_nav/tasks/$taskId/route.lazy.tsx} +20 -10
- package/src/routes/_nav/tasks/$taskId/route.tsx +43 -0
- package/src/routes/_nav/tasks/-components/ai-task-detail.tsx +307 -0
- package/src/routes/_nav/tasks/-components/ai-task-list.tsx +217 -0
- package/src/routes/_nav/tasks/-components/ai-task-status-badge.tsx +46 -0
- package/src/routes/_nav/tasks/-components/assigned-list.tsx +157 -0
- package/src/routes/_nav/tasks/-components/create-ai-task-dialog.tsx +228 -0
- package/src/routes/_nav/tasks/-components/source-ai-exec-dialog.tsx +175 -0
- package/src/routes/_nav/tasks/-components/task-constants.ts +115 -0
- package/src/routes/_nav/tasks/-components/use-task-filter.ts +64 -0
- package/src/routes/_nav/tasks/-tests/create-ai-task-dialog.test.tsx +175 -0
- package/src/routes/_nav/tasks/-tests/task-filter.test.ts +134 -0
- package/src/routes/_nav/tasks/-tests/task-styles.test.tsx +119 -0
- package/src/routes/_nav/tasks/index.lazy.tsx +114 -0
- package/src/routes/_nav/tasks/index.tsx +47 -0
- package/src/routes/_nav/{tags → tasks}/route.tsx +3 -3
- package/src/routes/_nav/test-cases/$caseId/route.lazy.tsx +3 -1
- package/src/routes/_nav/test-cases/$caseId/route.tsx +11 -2
- package/src/routes/api/chat.ts +23 -19
- package/src/routes/api/design/prototype/route.ts +2 -4
- package/src/routes/{_nav/acceptance-criteria/route.tsx → api/internal/collab-reindex.ts} +11 -8
- package/src/routes/api/knowledge/sync-github.ts +1 -1
- package/src/routes/design-files/$projectId/$artboardId/$filename.ts +9 -0
- package/src/routes/invite/$token/route.lazy.tsx +2 -2
- package/src/routes/share/$versionId/route.lazy.tsx +8 -0
- package/src/routes/share/compliance/$id/route.lazy.tsx +2 -2
- package/src/routes/share/compliance/$id/route.tsx +1 -1
- package/src/routes/share/law/$id/route.lazy.tsx +2 -2
- package/src/routes/share/law/$id/route.tsx +1 -1
- package/src/server/__tests__/bug-groups-two-level.test.ts +263 -0
- package/src/server/__tests__/builtin-project.test.ts +45 -1
- package/src/server/__tests__/csv-export.test.ts +319 -0
- package/src/server/__tests__/share-version.test.ts +7 -2
- package/src/server/__tests__/task-assigned-query.test.ts +232 -0
- package/src/server/__tests__/task-context-builder.test.ts +175 -0
- package/src/server/__tests__/task-executor.test.ts +349 -0
- package/src/server/__tests__/task-priority-map.test.ts +41 -0
- package/src/server/__tests__/task-serverfn-crud.test.ts +639 -0
- package/src/server/__tests__/task-serverfn-queries.test.ts +403 -0
- package/src/server/__tests__/testcase-board.test.ts +301 -0
- package/src/server/__tests__/testcase-bulk.test.ts +155 -0
- package/src/server/__tests__/testcase-list-scope.test.ts +215 -0
- package/src/server/ai-gateway/__tests__/chat-handler-prompt-key.test.ts +106 -0
- package/src/server/ai-gateway/__tests__/chat-stream-builder-sandbox.test.ts +9 -3
- package/src/server/ai-gateway/__tests__/conformance.test.ts +4 -3
- package/src/server/ai-gateway/adapter.ts +4 -2
- package/src/server/ai-gateway/chat-handler.ts +13 -5
- package/src/server/ai-gateway/chat-stream-builder.ts +12 -7
- package/src/server/ai-gateway/sandbox/__tests__/harness-sandbox.test.ts +88 -0
- package/src/server/ai-gateway/sandbox/harness-sandbox.ts +1 -1
- package/src/server/ai-gateway/story-handler.ts +7 -2
- package/src/server/ai-gateway/tools/plugin-lifecycle-defs.ts +8 -99
- package/src/server/ai-gateway/tools/plugin-tools.ts +6 -10
- package/src/server/ai-task/assigned-query.ts +167 -0
- package/src/server/ai-task/context-builder.ts +182 -0
- package/src/server/ai-task/executor.ts +296 -0
- package/src/server/ai-task/priority-map.ts +37 -0
- package/src/server/ai-task/types.ts +15 -0
- package/src/server/bug-report/__tests__/report-handler.test.ts +55 -4
- package/src/server/bug-report/report-handler.ts +54 -8
- package/src/server/builtin-project.ts +24 -0
- package/src/server/cloudflare/prisma-wasm-base64-stub.ts +5 -0
- package/src/server/collab/__tests__/collab-permissions.test.ts +412 -0
- package/src/server/collab/__tests__/knowledge-additions.test.ts +121 -0
- package/src/server/collab/__tests__/reindex-endpoint.test.ts +168 -0
- package/src/server/collab/__tests__/resolve.test.ts +249 -0
- package/src/server/collab/__tests__/touch-snapshot.test.ts +58 -0
- package/src/server/collab/__tests__/write-guard-sdk.test.ts +501 -0
- package/src/server/collab/__tests__/write-guard.test.ts +373 -0
- package/src/server/collab/knowledge-additions.ts +37 -0
- package/src/server/collab/reindex-endpoint.ts +75 -0
- package/src/server/collab/resolve.ts +122 -0
- package/src/server/collab/seed.ts +408 -0
- package/src/server/collab/ticket-utils.ts +107 -0
- package/src/server/collab/touch-snapshot.ts +55 -0
- package/src/server/collab/write-guard.ts +258 -0
- package/src/server/collab-permissions.ts +248 -0
- package/src/server/csv.ts +61 -0
- package/src/server/{knowledge-public → knowledge/public}/github-sync.ts +12 -2
- package/src/server/{knowledge-public → knowledge/public}/share-merge.ts +31 -8
- package/src/server/lib/__tests__/sequence-number-retry.test.ts +160 -0
- package/src/server/lib/rate-limit.ts +13 -2
- package/src/server/lib/sequence-number-retry.ts +85 -0
- package/src/server/mcp/__tests__/tools.test.ts +10 -0
- package/src/server/mcp/tools.ts +26 -2
- package/src/server/middlewares/auth-rate-limit.ts +18 -9
- package/src/server/notifications/event-bus.ts +3 -0
- package/src/server/serverFns/__tests__/collab-snapshot.test.ts +665 -0
- package/src/server/serverFns/__tests__/collab.test.ts +459 -0
- package/src/server/serverFns/__tests__/knowledge-delete.test.ts +167 -0
- package/src/server/serverFns/__tests__/ownership-merge.test.ts +34 -2
- package/src/server/serverFns/__tests__/read-write-merge.test.ts +2 -2
- package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +12 -2
- package/src/server/serverFns/__tests__/story-crud.test.ts +51 -11
- package/src/server/serverFns/__tests__/story-tree.test.ts +66 -34
- package/src/server/serverFns/__tests__/story-types.test.ts +14 -9
- package/src/server/serverFns/__tests__/update-bug-guard.test.ts +325 -0
- package/src/server/serverFns/__tests__/update-document-guard.test.ts +285 -0
- package/src/server/serverFns/__tests__/update-page-guard.test.ts +276 -0
- package/src/server/serverFns/__tests__/update-story-guard.test.ts +436 -0
- package/src/server/serverFns/__tests__/update-testcase-guard.test.ts +293 -0
- package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +25 -0
- package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +315 -15
- package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +36 -18
- package/src/server/serverFns/bug/__tests__/bug-list-query.test.ts +21 -0
- package/src/server/serverFns/bug/__tests__/bug-reactions.test.ts +335 -0
- package/src/server/serverFns/bug/__tests__/bug-reorder.test.ts +165 -0
- package/src/server/serverFns/bug/__tests__/bug-story.test.ts +4 -1
- package/src/server/serverFns/bug/bug-comments.ts +5 -0
- package/src/server/serverFns/bug/bug-crud.ts +321 -72
- package/src/server/serverFns/bug/bug-display-options.ts +11 -0
- package/src/server/serverFns/bug/bug-export.ts +137 -0
- package/src/server/serverFns/bug/bug-group-query.ts +16 -5
- package/src/server/serverFns/bug/bug-groups.ts +39 -15
- package/src/server/serverFns/bug/bug-list-query.ts +8 -1
- package/src/server/serverFns/bug/bug-reactions.ts +123 -0
- package/src/server/serverFns/bug/bug-reorder.ts +64 -0
- package/src/server/serverFns/bug/bug-story-bulk.ts +280 -0
- package/src/server/serverFns/bug/bug-story.ts +47 -39
- package/src/server/serverFns/bug/index.ts +6 -0
- package/src/server/serverFns/collab-presence.ts +119 -0
- package/src/server/serverFns/collab-snapshot.ts +201 -0
- package/src/server/serverFns/collab.ts +153 -0
- package/src/server/serverFns/design/__tests__/designs.test.ts +0 -2
- package/src/server/serverFns/design/designs.ts +0 -64
- package/src/server/serverFns/design/detail.ts +36 -2
- package/src/server/serverFns/design/pages.ts +73 -10
- package/src/server/serverFns/iteration/iteration-crud.ts +25 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/create.ts +3 -3
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/detail.ts +2 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/list.ts +3 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/refresh-favicon.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/share.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/sync-github.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/update.ts +5 -4
- package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/create.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/detail.ts +2 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/list.ts +3 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/share.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/update.ts +3 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/create.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/detail.ts +2 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/list.ts +3 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/share.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/update.ts +3 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/create.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/detail.ts +2 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/list.ts +3 -2
- package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/share.ts +1 -1
- package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/update.ts +3 -2
- package/src/server/serverFns/knowledge.ts +135 -19
- package/src/server/serverFns/project/module-overview.ts +1 -2
- package/src/server/serverFns/share/version.ts +7 -1
- package/src/server/serverFns/story/__tests__/bulk.test.ts +6 -0
- package/src/server/serverFns/story/__tests__/create-story-internal.test.ts +112 -3
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +77 -6
- package/src/server/serverFns/story/__tests__/story-crud-date-key.test.ts +187 -0
- package/src/server/serverFns/story/__tests__/story-gantt-deps.test.ts +163 -0
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +4 -0
- package/src/server/serverFns/story/__tests__/story-list-query.test.ts +20 -0
- package/src/server/serverFns/story/__tests__/story-search.test.ts +5 -0
- package/src/server/serverFns/story/__tests__/story-transfer.test.ts +64 -1
- package/src/server/serverFns/story/index.ts +2 -2
- package/src/server/serverFns/story/story-assoc.ts +44 -14
- package/src/server/serverFns/story/story-crud/archive.ts +52 -28
- package/src/server/serverFns/story/story-crud/bulk.ts +14 -4
- package/src/server/serverFns/story/story-crud/create-story-internal.ts +39 -21
- package/src/server/serverFns/story/story-crud.ts +283 -116
- package/src/server/serverFns/story/story-dependencies.ts +31 -0
- package/src/server/serverFns/story/story-groups.ts +15 -11
- package/src/server/serverFns/story/story-list-query.ts +16 -4
- package/src/server/serverFns/story/story-search.ts +9 -6
- package/src/server/serverFns/story/story-transfer.ts +99 -42
- package/src/server/serverFns/story/story-tree.ts +70 -48
- package/src/server/serverFns/story-display-options.ts +21 -10
- package/src/server/serverFns/story-view.ts +2 -2
- package/src/server/serverFns/tasks/task-crud.ts +264 -0
- package/src/server/serverFns/tasks/task-queries.ts +172 -0
- package/src/server/serverFns/testcase/__tests__/generation.test.ts +3 -0
- package/src/server/serverFns/testcase/__tests__/stats.test.ts +18 -4
- package/src/server/serverFns/testcase/__tests__/testcase-board.test.ts +113 -0
- package/src/server/serverFns/testcase/__tests__/testcase-crud.test.ts +62 -14
- package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +32 -10
- package/src/server/serverFns/testcase/bulk.ts +195 -0
- package/src/server/serverFns/testcase/generation.ts +10 -0
- package/src/server/serverFns/testcase/stats.ts +22 -8
- package/src/server/serverFns/testcase/testcase-board.ts +198 -0
- package/src/server/serverFns/testcase/testcase-crud.ts +165 -33
- package/src/server/serverFns/testcase/testcase-display-options.ts +166 -0
- package/src/server/serverFns/testcase/testcase-export.ts +125 -0
- package/src/server/serverFns/testcase/testcase-filter-views.ts +36 -9
- package/src/server/serverFns/testcase/testcase-groups.ts +27 -23
- package/src/server/serverFns/testcase/testcase-reorder.ts +66 -0
- package/src/server/story/__tests__/date-key.test.ts +57 -0
- package/src/server/story/date-key.ts +36 -0
- package/src/server/story/types.ts +3 -7
- package/src/server/testcase/__tests__/list-query.test.ts +269 -0
- package/src/server/testcase/list-query.ts +145 -0
- package/src/server-entry.ts +32 -0
- package/src/shared/bug/__tests__/constants.test.ts +44 -0
- package/src/shared/bug/constants.ts +37 -1
- package/src/start.ts +7 -2
- package/src/styles/globals.css +24 -3
- package/src/types/project.ts +2 -3
- package/vite.shared.ts +110 -3
- package/src/__tests__/tag-badge.test.tsx +0 -41
- package/src/components/bug/__tests__/bug-list-table.test.tsx +0 -289
- package/src/components/bug/bug-list-table.tsx +0 -187
- package/src/components/bug/columns.tsx +0 -193
- package/src/components/bug/filters/add-filter-dropdown.tsx +0 -70
- package/src/components/bug/filters/applied-filters.tsx +0 -214
- package/src/components/bug/filters/filter-condition.tsx +0 -151
- package/src/components/bug/filters/filter-row.tsx +0 -104
- package/src/components/bug/filters/filter-toggle.tsx +0 -61
- package/src/components/bug/filters/filter-types.ts +0 -146
- package/src/components/story/detail/story-detail-toolbar.tsx +0 -382
- package/src/components/story/filters/add-filter-dropdown.tsx +0 -70
- package/src/components/story/filters/applied-filters.tsx +0 -225
- package/src/components/story/filters/filter-condition.tsx +0 -152
- package/src/components/story/filters/filter-row.tsx +0 -116
- package/src/components/story/filters/filter-toggle.tsx +0 -60
- package/src/components/story/filters/filter-types.ts +0 -120
- package/src/components/story/filters/filter-value-input.tsx +0 -182
- package/src/components/story/views/kanban-board.tsx +0 -464
- package/src/components/story/views/kanban-card.tsx +0 -100
- package/src/components/tags/__tests__/tags.test.ts +0 -388
- package/src/components/tags/nav-tag.tsx +0 -121
- package/src/components/tags/pages/tag-settings-page.tsx +0 -32
- package/src/components/tags/pages/tags-index-page.tsx +0 -32
- package/src/components/tags/tag-badge.tsx +0 -56
- package/src/components/tags/tag-color-classes.ts +0 -61
- package/src/components/tags/tag-color-picker.tsx +0 -54
- package/src/components/tags/tag-components-index.ts +0 -23
- package/src/components/tags/tag-manager.tsx +0 -208
- package/src/components/tags/tag-selector.tsx +0 -164
- package/src/components/testcase/__tests__/table-view.test.tsx +0 -328
- package/src/components/testcase/plans/add-to-plan-bar.tsx +0 -64
- package/src/components/testcase/table/columns.tsx +0 -170
- package/src/components/testcase/table/table-view.tsx +0 -152
- package/src/lib/tag-colors.ts +0 -35
- package/src/routes/_nav/tags/$id/route.lazy.tsx +0 -23
- package/src/routes/_nav/tags/$id/route.tsx +0 -20
- package/src/routes/_nav/tags/index.lazy.tsx +0 -23
- package/src/routes/_nav/tags/index.tsx +0 -20
- package/src/routes/_nav/tags/settings/route.lazy.tsx +0 -23
- package/src/routes/_nav/tags/settings/route.tsx +0 -23
- package/src/server/serverFns/tags.ts +0 -138
- /package/src/components/{knowledge-public → knowledge/public}/bookmark-card.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/category-icon.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/chip-utils.ts +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/knowledge-card-grid.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/knowledge-filters.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/knowledge-search-bar.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/markdown-editor.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/markdown-view.tsx +0 -0
- /package/src/components/{knowledge-public → knowledge/public}/tag-filter.tsx +0 -0
- /package/src/components/{story/detail → shared-kit/comments}/emoji-picker.tsx +0 -0
- /package/src/components/story/{schemas/story-create.ts → story-create-schema.ts} +0 -0
- /package/src/server/{knowledge-public → knowledge/public}/canonical-id.ts +0 -0
- /package/src/server/{knowledge-public → knowledge/public}/favicon.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/categories.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/delete.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/favorite.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/unshare.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/delete.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/public.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/unshare.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/delete.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/dictionary/unshare.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/entity-tags.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/favorite.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/delete.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/public.ts +0 -0
- /package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/unshare.ts +0 -0
|
@@ -0,0 +1,1822 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
useSyncExternalStore,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
type RefObject,
|
|
11
|
+
} from "react"
|
|
12
|
+
import {
|
|
13
|
+
mergeGanttI18n,
|
|
14
|
+
type GanttI18nConfig,
|
|
15
|
+
type GanttI18nOverrides,
|
|
16
|
+
} from "@/components/reui/gantt/gantt-i18n"
|
|
17
|
+
import {
|
|
18
|
+
buildEventIndex,
|
|
19
|
+
defaultEventOrder,
|
|
20
|
+
eventsOverlap,
|
|
21
|
+
findResource,
|
|
22
|
+
getGanttDateRange,
|
|
23
|
+
getRangeKey,
|
|
24
|
+
stepGanttDate,
|
|
25
|
+
toZoned,
|
|
26
|
+
type GanttIndex,
|
|
27
|
+
type WeekStartsOn,
|
|
28
|
+
} from "@/components/reui/gantt/gantt-lib"
|
|
29
|
+
import type {
|
|
30
|
+
GanttBarId,
|
|
31
|
+
GanttBaseline,
|
|
32
|
+
GanttBaselineVariance,
|
|
33
|
+
GanttDateRange,
|
|
34
|
+
GanttDragState,
|
|
35
|
+
GanttEvent,
|
|
36
|
+
GanttInteractions,
|
|
37
|
+
GanttOccurrence,
|
|
38
|
+
GanttOffDaysConfig,
|
|
39
|
+
GanttOverlapPolicy,
|
|
40
|
+
GanttProposedUpdate,
|
|
41
|
+
GanttRangeInfo,
|
|
42
|
+
GanttResource,
|
|
43
|
+
GanttResourceReorder,
|
|
44
|
+
GanttRowAlign,
|
|
45
|
+
GanttScale,
|
|
46
|
+
GanttScheduleMode,
|
|
47
|
+
GanttSegment,
|
|
48
|
+
GanttSelection,
|
|
49
|
+
GanttSlotDraft,
|
|
50
|
+
GanttSlotInfo,
|
|
51
|
+
GanttState,
|
|
52
|
+
GanttUpdateResult,
|
|
53
|
+
} from "@/components/reui/gantt/gantt-types"
|
|
54
|
+
import { mergeProps } from "@base-ui/react/merge-props"
|
|
55
|
+
import { useRender } from "@base-ui/react/use-render"
|
|
56
|
+
import type { Locale } from "date-fns"
|
|
57
|
+
|
|
58
|
+
import { cn } from "cn"
|
|
59
|
+
|
|
60
|
+
const DEFAULT_INTERACTIONS: GanttInteractions = {
|
|
61
|
+
drag: true,
|
|
62
|
+
resize: true,
|
|
63
|
+
selectSlot: true,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Infinite-scroll growth cap, in whole periods per side. */
|
|
67
|
+
const MAX_RANGE_WINDOW = 12
|
|
68
|
+
|
|
69
|
+
/** A node holds as many concurrent schedules as it needs unless told otherwise. */
|
|
70
|
+
const DEFAULT_SCHEDULE_MODE: GanttScheduleMode = "multiple"
|
|
71
|
+
|
|
72
|
+
/** Tree label sits on the first schedule's baseline, not the grown row's middle. */
|
|
73
|
+
const DEFAULT_ROW_ALIGN: GanttRowAlign = "start"
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A node's cardinality: its own override wins over the view-level default.
|
|
77
|
+
* Shared by the layout pass and the gesture engine so both read one rule.
|
|
78
|
+
*/
|
|
79
|
+
function resolveScheduleMode(
|
|
80
|
+
node: GanttResource | null | undefined,
|
|
81
|
+
scheduleMode: GanttScheduleMode | undefined
|
|
82
|
+
): GanttScheduleMode {
|
|
83
|
+
return node?.scheduleMode ?? scheduleMode ?? DEFAULT_SCHEDULE_MODE
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const EMPTY_SELECTION: GanttSelection = { eventKeys: [], slot: null }
|
|
87
|
+
|
|
88
|
+
interface GanttCallbacks<TData = unknown> {
|
|
89
|
+
onEventClick?: (
|
|
90
|
+
occurrence: GanttOccurrence<TData>,
|
|
91
|
+
e: React.MouseEvent
|
|
92
|
+
) => void
|
|
93
|
+
onEventDoubleClick?: (
|
|
94
|
+
occurrence: GanttOccurrence<TData>,
|
|
95
|
+
e: React.MouseEvent
|
|
96
|
+
) => void
|
|
97
|
+
onEventUpdate?: (update: GanttProposedUpdate<TData>) => GanttUpdateResult
|
|
98
|
+
canDropEvent?: (update: GanttProposedUpdate<TData>) => boolean
|
|
99
|
+
onSlotClick?: (slot: GanttSlotInfo, e: React.MouseEvent) => void
|
|
100
|
+
onSelectSlot?: (slot: GanttSlotDraft) => void
|
|
101
|
+
canSelectSlot?: (slot: GanttSlotDraft) => boolean
|
|
102
|
+
/** Fires when the "add task" hint is activated; create a new tree row. */
|
|
103
|
+
onCreateTask?: (ctx: { parentId: string | null; index: number }) => void
|
|
104
|
+
/**
|
|
105
|
+
* Gates the "add task" hint. The shipped view offers root-level creation
|
|
106
|
+
* only (parentId = null); parentId stays in the contract for group-level
|
|
107
|
+
* affordances a consumer builds via its own UI + onCreateTask.
|
|
108
|
+
*/
|
|
109
|
+
canCreateTask?: (ctx: { parentId: string | null }) => boolean
|
|
110
|
+
/** Click on a tree row's surface (chevron/checkbox/grip clicks excluded). */
|
|
111
|
+
onResourceClick?: (ctx: GanttColumnContext, e: React.MouseEvent) => void
|
|
112
|
+
onResourceDoubleClick?: (ctx: GanttColumnContext, e: React.MouseEvent) => void
|
|
113
|
+
onRangeChange?: (info: GanttRangeInfo) => void
|
|
114
|
+
onScaleChange?: (scale: GanttScale) => void
|
|
115
|
+
onDateChange?: (date: Date) => void
|
|
116
|
+
onSelectionChange?: (selection: GanttSelection) => void
|
|
117
|
+
onInteractionsChange?: (interactions: GanttInteractions) => void
|
|
118
|
+
onEventsChange?: (events: GanttEvent<TData>[]) => void
|
|
119
|
+
/**
|
|
120
|
+
* Commit gate for timeline resource-row drag reorder. Return false to
|
|
121
|
+
* reject; apply the move by adopting proposal.resources into your
|
|
122
|
+
* `resources` state (controlled - the calendar never self-mutates).
|
|
123
|
+
*/
|
|
124
|
+
onResourceReorder?: (proposal: GanttResourceReorder) => void | false
|
|
125
|
+
/** Live validity predicate while a resource row is being dragged. */
|
|
126
|
+
canReorderResource?: (proposal: GanttResourceReorder) => boolean
|
|
127
|
+
/**
|
|
128
|
+
* Fires when a reorder gesture is released on a position rejected by
|
|
129
|
+
* `canReorderResource` (e.g. a pinned row). Use it to explain the rejection
|
|
130
|
+
* (a toast) - the destructive drop indicator already shows it live.
|
|
131
|
+
*/
|
|
132
|
+
onResourceReorderReject?: (proposal: GanttResourceReorder) => void
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface UseGanttStateOptions<TData = unknown> extends GanttCallbacks<TData> {
|
|
136
|
+
events?: GanttEvent<TData>[]
|
|
137
|
+
defaultEvents?: GanttEvent<TData>[]
|
|
138
|
+
scale?: GanttScale
|
|
139
|
+
defaultScale?: GanttScale
|
|
140
|
+
date?: Date
|
|
141
|
+
defaultDate?: Date
|
|
142
|
+
selection?: GanttSelection
|
|
143
|
+
defaultSelection?: GanttSelection
|
|
144
|
+
interactions?: Partial<GanttInteractions>
|
|
145
|
+
defaultInteractions?: Partial<GanttInteractions>
|
|
146
|
+
loading?: boolean
|
|
147
|
+
timeZone?: string
|
|
148
|
+
locale?: Locale
|
|
149
|
+
weekStartsOn?: WeekStartsOn
|
|
150
|
+
slotDuration?: number
|
|
151
|
+
snapDuration?: number
|
|
152
|
+
i18n?: GanttI18nOverrides
|
|
153
|
+
/**
|
|
154
|
+
* Hard travel bounds for infinite scrolling; either side may be omitted
|
|
155
|
+
* for unlimited travel in that direction.
|
|
156
|
+
*/
|
|
157
|
+
rangeBounds?: { min?: Date; max?: Date }
|
|
158
|
+
/** Pointer-activation threshold overrides for drag/resize/create. */
|
|
159
|
+
activation?: GanttActivationConfig
|
|
160
|
+
/**
|
|
161
|
+
* Infinite-scroll growth cap in whole periods per side; past it the
|
|
162
|
+
* anchor slides instead (DOM stays bounded). Default 12.
|
|
163
|
+
*/
|
|
164
|
+
maxRangeWindow?: number
|
|
165
|
+
/** Tree nodes of the gantt (GanttNode is the preferred type name). */
|
|
166
|
+
resources?: GanttResource[]
|
|
167
|
+
/**
|
|
168
|
+
* What a gesture may do when it would overlap another schedule in the SAME
|
|
169
|
+
* node: "allow" (default), "clamp" to the neighbour's edge, or "reject".
|
|
170
|
+
* Policy only - overlapping data always renders. A node in "single"
|
|
171
|
+
* scheduleMode rejects regardless.
|
|
172
|
+
*/
|
|
173
|
+
overlap?: GanttOverlapPolicy
|
|
174
|
+
/**
|
|
175
|
+
* Make canDropEvent binding: releasing a gesture whose last verdict was
|
|
176
|
+
* invalid reverts it, like the "reject" overlap policy, instead of
|
|
177
|
+
* committing anyway. Default false - canDropEvent alone stays advisory
|
|
178
|
+
* (it styles the ghost; onEventUpdate is the commit gate).
|
|
179
|
+
*/
|
|
180
|
+
enforceCanDrop?: boolean
|
|
181
|
+
getEventPriority?: (event: GanttEvent<TData>) => number
|
|
182
|
+
eventOrder?: (a: GanttOccurrence<TData>, b: GanttOccurrence<TData>) => number
|
|
183
|
+
getOccurrences?: (
|
|
184
|
+
event: GanttEvent<TData>,
|
|
185
|
+
range: GanttDateRange,
|
|
186
|
+
ctx: { timeZone: string }
|
|
187
|
+
) => Array<{ start: Date; end: Date }> | null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolved configuration: every UseGanttStateOptions field except the
|
|
192
|
+
* controlled/uncontrolled state pairs, with defaults applied and i18n merged.
|
|
193
|
+
* Read via ref semantics - callback identity changes never re-render the grid.
|
|
194
|
+
*/
|
|
195
|
+
interface GanttSettings<TData = unknown> extends GanttCallbacks<TData> {
|
|
196
|
+
timeZone: string
|
|
197
|
+
locale?: Locale
|
|
198
|
+
weekStartsOn: WeekStartsOn
|
|
199
|
+
slotDuration: number
|
|
200
|
+
snapDuration: number
|
|
201
|
+
i18n: GanttI18nConfig
|
|
202
|
+
rangeBounds?: { min?: Date; max?: Date }
|
|
203
|
+
activation?: GanttActivationConfig
|
|
204
|
+
maxRangeWindow?: number
|
|
205
|
+
resources: GanttResource[]
|
|
206
|
+
overlap: GanttOverlapPolicy
|
|
207
|
+
enforceCanDrop?: boolean
|
|
208
|
+
getEventPriority: (event: GanttEvent<TData>) => number
|
|
209
|
+
eventOrder: (a: GanttOccurrence<TData>, b: GanttOccurrence<TData>) => number
|
|
210
|
+
getOccurrences?: (
|
|
211
|
+
event: GanttEvent<TData>,
|
|
212
|
+
range: GanttDateRange,
|
|
213
|
+
ctx: { timeZone: string }
|
|
214
|
+
) => Array<{ start: Date; end: Date }> | null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface GanttApi<TData = unknown> {
|
|
218
|
+
next(): void
|
|
219
|
+
prev(): void
|
|
220
|
+
today(): void
|
|
221
|
+
goTo(date: Date): void
|
|
222
|
+
setScale(scale: GanttScale): void
|
|
223
|
+
getEvents(): GanttEvent<TData>[]
|
|
224
|
+
getEvent(id: GanttBarId): GanttEvent<TData> | undefined
|
|
225
|
+
setEvents(events: GanttEvent<TData>[]): void
|
|
226
|
+
addEvent(event: GanttEvent<TData>): void
|
|
227
|
+
updateEvent(id: GanttBarId, patch: Partial<GanttEvent<TData>>): void
|
|
228
|
+
removeEvent(id: GanttBarId): void
|
|
229
|
+
getOccurrences(range?: GanttDateRange): GanttOccurrence<TData>[]
|
|
230
|
+
findOverlapping(candidate: {
|
|
231
|
+
start: Date
|
|
232
|
+
end: Date
|
|
233
|
+
excludeEventId?: string
|
|
234
|
+
}): GanttOccurrence<TData>[]
|
|
235
|
+
select(selection: Partial<GanttSelection>): void
|
|
236
|
+
selectEvent(key: string, opts?: { additive?: boolean }): void
|
|
237
|
+
clearSelection(): void
|
|
238
|
+
setInteractions(patch: Partial<GanttInteractions>): void
|
|
239
|
+
getVisibleRange(): GanttDateRange
|
|
240
|
+
getActiveRange(): GanttDateRange
|
|
241
|
+
/** TZDate in the gantt's display time zone. */
|
|
242
|
+
toZoned(date: Date): Date
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Cross-file plumbing for sibling view/interaction modules; not public API. */
|
|
246
|
+
interface GanttInternals<TData = unknown> {
|
|
247
|
+
getIndex(): GanttIndex<TData>
|
|
248
|
+
setDrag(drag: GanttDragState<TData> | null): void
|
|
249
|
+
setSlotDraft(draft: GanttSlotDraft | null): void
|
|
250
|
+
applyProposedUpdate(update: GanttProposedUpdate<TData>): boolean
|
|
251
|
+
getSettingsVersion(): number
|
|
252
|
+
/**
|
|
253
|
+
* Grow visibleRange by whole periods for infinite scrolling; resets on
|
|
254
|
+
* date/scale changes. Returns false once the growth cap is reached.
|
|
255
|
+
*/
|
|
256
|
+
extendRange(direction: "before" | "after"): boolean
|
|
257
|
+
/**
|
|
258
|
+
* True when the LAST anchor-date change was an extendRange window slide
|
|
259
|
+
* (not a navigation) - the view keeps its scroll guard across slides.
|
|
260
|
+
*/
|
|
261
|
+
didAnchorSlide(): boolean
|
|
262
|
+
/** View reports the visible-center instant (or null) for the nav title. */
|
|
263
|
+
setViewportCenter(date: Date | null): void
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface GanttInstance<TData = unknown> {
|
|
267
|
+
getState(): GanttState<TData>
|
|
268
|
+
subscribe(listener: () => void): () => void
|
|
269
|
+
api: GanttApi<TData>
|
|
270
|
+
settings: GanttSettings<TData>
|
|
271
|
+
internals: GanttInternals<TData>
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function resolveSettings<TData>(
|
|
275
|
+
options: UseGanttStateOptions<TData>
|
|
276
|
+
): GanttSettings<TData> {
|
|
277
|
+
const {
|
|
278
|
+
// strip state pairs; the rest flows into settings
|
|
279
|
+
events: _e,
|
|
280
|
+
defaultEvents: _de,
|
|
281
|
+
scale: _v,
|
|
282
|
+
defaultScale: _dv,
|
|
283
|
+
date: _d,
|
|
284
|
+
defaultDate: _dd,
|
|
285
|
+
selection: _s,
|
|
286
|
+
defaultSelection: _ds,
|
|
287
|
+
interactions: _i,
|
|
288
|
+
defaultInteractions: _di,
|
|
289
|
+
loading: _l,
|
|
290
|
+
...rest
|
|
291
|
+
} = options
|
|
292
|
+
const getEventPriority =
|
|
293
|
+
options.getEventPriority ??
|
|
294
|
+
((event: GanttEvent<TData>) => event.priority ?? 0)
|
|
295
|
+
return {
|
|
296
|
+
...rest,
|
|
297
|
+
timeZone:
|
|
298
|
+
options.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
299
|
+
locale: options.locale,
|
|
300
|
+
// locale-first default: a de/fr locale gets Monday weeks without also
|
|
301
|
+
// having to set weekStartsOn; an explicit weekStartsOn always wins
|
|
302
|
+
weekStartsOn:
|
|
303
|
+
options.weekStartsOn ?? options.locale?.options?.weekStartsOn ?? 0,
|
|
304
|
+
slotDuration: options.slotDuration ?? 30,
|
|
305
|
+
snapDuration: options.snapDuration ?? 15,
|
|
306
|
+
i18n: mergeGanttI18n(options.i18n),
|
|
307
|
+
rangeBounds: options.rangeBounds,
|
|
308
|
+
resources: options.resources ?? [],
|
|
309
|
+
overlap: options.overlap ?? "allow",
|
|
310
|
+
getEventPriority,
|
|
311
|
+
// priority-aware default: higher getEventPriority packs/orders first
|
|
312
|
+
eventOrder:
|
|
313
|
+
options.eventOrder ??
|
|
314
|
+
((a, b) =>
|
|
315
|
+
getEventPriority(b.event) - getEventPriority(a.event) ||
|
|
316
|
+
defaultEventOrder(a, b)),
|
|
317
|
+
getOccurrences: options.getOccurrences,
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const warned = new Set<string>()
|
|
322
|
+
function warnOnce(key: string, message: string) {
|
|
323
|
+
if (process.env.NODE_ENV !== "production" && !warned.has(key)) {
|
|
324
|
+
warned.add(key)
|
|
325
|
+
console.warn(`[gantt] ${message}`)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface GanttStore<TData> {
|
|
330
|
+
instance: GanttInstance<TData>
|
|
331
|
+
setOptions(next: UseGanttStateOptions<TData>): boolean
|
|
332
|
+
notify(): void
|
|
333
|
+
emitRangeIfChanged(): void
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function createGanttStore<TData>(
|
|
337
|
+
initial: UseGanttStateOptions<TData>
|
|
338
|
+
): GanttStore<TData> {
|
|
339
|
+
let options = initial
|
|
340
|
+
let settings = resolveSettings(initial)
|
|
341
|
+
let settingsVersion = 0
|
|
342
|
+
|
|
343
|
+
const listeners = new Set<() => void>()
|
|
344
|
+
|
|
345
|
+
const internal = {
|
|
346
|
+
scale: initial.defaultScale ?? "day",
|
|
347
|
+
date: initial.defaultDate ?? new Date(),
|
|
348
|
+
events: initial.defaultEvents ?? [],
|
|
349
|
+
selection: initial.defaultSelection ?? EMPTY_SELECTION,
|
|
350
|
+
interactions: { ...DEFAULT_INTERACTIONS, ...initial.defaultInteractions },
|
|
351
|
+
drag: null as GanttDragState<TData> | null,
|
|
352
|
+
slotDraft: null as GanttSlotDraft | null,
|
|
353
|
+
/** Whole extra periods rendered on each side (infinite scroll). */
|
|
354
|
+
rangeWindow: { before: 0, after: 0 },
|
|
355
|
+
/** Visible-center instant reported by the view; drives the nav title. */
|
|
356
|
+
viewportCenter: null as Date | null,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let snapshot: GanttState<TData> | null = null
|
|
360
|
+
let indexCache: {
|
|
361
|
+
events: GanttEvent<TData>[]
|
|
362
|
+
rangeKey: string
|
|
363
|
+
timeZone: string
|
|
364
|
+
index: GanttIndex<TData>
|
|
365
|
+
} | null = null
|
|
366
|
+
let lastEmittedRangeKey: string | null = null
|
|
367
|
+
/** Whether the last anchor change came from an extendRange window slide. */
|
|
368
|
+
let lastAnchorChangeWasSlide = false
|
|
369
|
+
|
|
370
|
+
const invalidate = () => {
|
|
371
|
+
snapshot = null
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const notify = () => {
|
|
375
|
+
listeners.forEach((listener) => listener())
|
|
376
|
+
emitRangeIfChanged()
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const getState = (): GanttState<TData> => {
|
|
380
|
+
if (snapshot) return snapshot
|
|
381
|
+
const scale = options.scale ?? internal.scale
|
|
382
|
+
const date = options.date ?? internal.date
|
|
383
|
+
const rangeOpts = {
|
|
384
|
+
timeZone: settings.timeZone,
|
|
385
|
+
weekStartsOn: settings.weekStartsOn,
|
|
386
|
+
}
|
|
387
|
+
const { visibleRange: baseRange, activeRange } = getGanttDateRange(
|
|
388
|
+
scale,
|
|
389
|
+
date,
|
|
390
|
+
rangeOpts
|
|
391
|
+
)
|
|
392
|
+
// Infinite scroll: widen by whole periods; the anchor period stays put
|
|
393
|
+
const { before, after } = internal.rangeWindow
|
|
394
|
+
let visibleRange = baseRange
|
|
395
|
+
if (before > 0 || after > 0) {
|
|
396
|
+
let earlier = date
|
|
397
|
+
for (let i = 0; i < before; i++) {
|
|
398
|
+
earlier = stepGanttDate(scale, earlier, -1, rangeOpts)
|
|
399
|
+
}
|
|
400
|
+
let later = date
|
|
401
|
+
for (let i = 0; i < after; i++) {
|
|
402
|
+
later = stepGanttDate(scale, later, 1, rangeOpts)
|
|
403
|
+
}
|
|
404
|
+
visibleRange = {
|
|
405
|
+
start: getGanttDateRange(scale, earlier, rangeOpts).visibleRange.start,
|
|
406
|
+
end: getGanttDateRange(scale, later, rangeOpts).visibleRange.end,
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
snapshot = {
|
|
410
|
+
scale,
|
|
411
|
+
date,
|
|
412
|
+
visibleRange,
|
|
413
|
+
activeRange,
|
|
414
|
+
events: options.events ?? internal.events,
|
|
415
|
+
selection: options.selection ?? internal.selection,
|
|
416
|
+
interactions: options.interactions
|
|
417
|
+
? { ...DEFAULT_INTERACTIONS, ...options.interactions }
|
|
418
|
+
: internal.interactions,
|
|
419
|
+
loading: options.loading ?? false,
|
|
420
|
+
drag: internal.drag,
|
|
421
|
+
slotDraft: internal.slotDraft,
|
|
422
|
+
viewportCenter: internal.viewportCenter,
|
|
423
|
+
}
|
|
424
|
+
return snapshot
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const emitRangeIfChanged = () => {
|
|
428
|
+
if (!settings.onRangeChange) return
|
|
429
|
+
const state = getState()
|
|
430
|
+
const key = `${state.scale}:${getRangeKey(state.visibleRange)}:${settings.timeZone}`
|
|
431
|
+
if (key === lastEmittedRangeKey) return
|
|
432
|
+
lastEmittedRangeKey = key
|
|
433
|
+
settings.onRangeChange({
|
|
434
|
+
range: state.visibleRange,
|
|
435
|
+
activeRange: state.activeRange,
|
|
436
|
+
scale: state.scale,
|
|
437
|
+
date: state.date,
|
|
438
|
+
timeZone: settings.timeZone,
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
type ControlledKey =
|
|
443
|
+
| "scale"
|
|
444
|
+
| "date"
|
|
445
|
+
| "events"
|
|
446
|
+
| "selection"
|
|
447
|
+
| "interactions"
|
|
448
|
+
|
|
449
|
+
const setField = <K extends ControlledKey>(
|
|
450
|
+
key: K,
|
|
451
|
+
value: GanttState<TData>[K extends "events" ? "events" : K]
|
|
452
|
+
) => {
|
|
453
|
+
const controlled = options[key] !== undefined
|
|
454
|
+
if (key === "date" || key === "scale") {
|
|
455
|
+
// value-equal sets are no-ops: they must not touch store state (the
|
|
456
|
+
// controlled path would mutate without notify) nor drop infinite-
|
|
457
|
+
// scroll growth for a navigation that never happened
|
|
458
|
+
const current = getState()[key]
|
|
459
|
+
const same =
|
|
460
|
+
key === "date"
|
|
461
|
+
? (current as Date).getTime() === (value as Date).getTime()
|
|
462
|
+
: current === value
|
|
463
|
+
if (same) return
|
|
464
|
+
// navigating re-anchors the axis; drop any infinite-scroll growth and
|
|
465
|
+
// let the title follow the anchor again until the user scrolls
|
|
466
|
+
internal.rangeWindow = { before: 0, after: 0 }
|
|
467
|
+
internal.viewportCenter = null
|
|
468
|
+
lastAnchorChangeWasSlide = false
|
|
469
|
+
invalidate()
|
|
470
|
+
}
|
|
471
|
+
if (!controlled) {
|
|
472
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
473
|
+
;(internal as any)[key] = value
|
|
474
|
+
invalidate()
|
|
475
|
+
}
|
|
476
|
+
const callbacks: Record<ControlledKey, ((v: never) => void) | undefined> = {
|
|
477
|
+
scale: settings.onScaleChange as never,
|
|
478
|
+
date: settings.onDateChange as never,
|
|
479
|
+
events: settings.onEventsChange as never,
|
|
480
|
+
selection: settings.onSelectionChange as never,
|
|
481
|
+
interactions: settings.onInteractionsChange as never,
|
|
482
|
+
}
|
|
483
|
+
callbacks[key]?.(value as never)
|
|
484
|
+
if (!controlled) notify()
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const applyProposedUpdate = (
|
|
488
|
+
update: GanttProposedUpdate<TData>,
|
|
489
|
+
// extra non-timing fields committed in the SAME events emission: a second
|
|
490
|
+
// setField pass would read stale controlled options.events and emit an
|
|
491
|
+
// array without the timing change
|
|
492
|
+
extra?: Partial<GanttEvent<TData>>
|
|
493
|
+
): boolean => {
|
|
494
|
+
const result = settings.onEventUpdate?.(update)
|
|
495
|
+
if (result === false) return false
|
|
496
|
+
const adjusted: Partial<GanttEvent<TData>> =
|
|
497
|
+
result && typeof result === "object"
|
|
498
|
+
? {
|
|
499
|
+
start: result.start ?? update.start,
|
|
500
|
+
end: result.end ?? update.end,
|
|
501
|
+
allDay: result.allDay ?? update.allDay,
|
|
502
|
+
}
|
|
503
|
+
: { start: update.start, end: update.end, allDay: update.allDay }
|
|
504
|
+
if (update.resourceId !== undefined) adjusted.resourceId = update.resourceId
|
|
505
|
+
const merged = extra ? { ...extra, ...adjusted } : adjusted
|
|
506
|
+
const events = getState().events
|
|
507
|
+
const next = events.map((event) =>
|
|
508
|
+
event.id === update.event.id ? { ...event, ...merged } : event
|
|
509
|
+
)
|
|
510
|
+
setField("events", next)
|
|
511
|
+
return true
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const getIndex = (): GanttIndex<TData> => {
|
|
515
|
+
const state = getState()
|
|
516
|
+
const rangeKey = getRangeKey(state.visibleRange)
|
|
517
|
+
if (
|
|
518
|
+
indexCache &&
|
|
519
|
+
indexCache.events === state.events &&
|
|
520
|
+
indexCache.rangeKey === rangeKey &&
|
|
521
|
+
indexCache.timeZone === settings.timeZone
|
|
522
|
+
) {
|
|
523
|
+
return indexCache.index
|
|
524
|
+
}
|
|
525
|
+
const index = buildEventIndex(state.events, state.visibleRange, {
|
|
526
|
+
timeZone: settings.timeZone,
|
|
527
|
+
eventOrder: settings.eventOrder,
|
|
528
|
+
getOccurrences: settings.getOccurrences,
|
|
529
|
+
})
|
|
530
|
+
indexCache = {
|
|
531
|
+
events: state.events,
|
|
532
|
+
rangeKey,
|
|
533
|
+
timeZone: settings.timeZone,
|
|
534
|
+
index,
|
|
535
|
+
}
|
|
536
|
+
return index
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Anchor clamp: navigation may never leave the configured bounds. */
|
|
540
|
+
const clampToBounds = (date: Date): Date => {
|
|
541
|
+
const bounds = settings.rangeBounds
|
|
542
|
+
if (!bounds) return date
|
|
543
|
+
if (bounds.min && date.getTime() < bounds.min.getTime()) return bounds.min
|
|
544
|
+
if (bounds.max && date.getTime() > bounds.max.getTime()) return bounds.max
|
|
545
|
+
return date
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const api: GanttApi<TData> = {
|
|
549
|
+
next() {
|
|
550
|
+
const state = getState()
|
|
551
|
+
setField(
|
|
552
|
+
"date",
|
|
553
|
+
clampToBounds(
|
|
554
|
+
stepGanttDate(state.scale, state.date, 1, {
|
|
555
|
+
timeZone: settings.timeZone,
|
|
556
|
+
})
|
|
557
|
+
)
|
|
558
|
+
)
|
|
559
|
+
},
|
|
560
|
+
prev() {
|
|
561
|
+
const state = getState()
|
|
562
|
+
setField(
|
|
563
|
+
"date",
|
|
564
|
+
clampToBounds(
|
|
565
|
+
stepGanttDate(state.scale, state.date, -1, {
|
|
566
|
+
timeZone: settings.timeZone,
|
|
567
|
+
})
|
|
568
|
+
)
|
|
569
|
+
)
|
|
570
|
+
},
|
|
571
|
+
today() {
|
|
572
|
+
setField("date", clampToBounds(new Date()))
|
|
573
|
+
},
|
|
574
|
+
goTo(date) {
|
|
575
|
+
setField("date", clampToBounds(date))
|
|
576
|
+
},
|
|
577
|
+
setScale(scale) {
|
|
578
|
+
setField("scale", scale)
|
|
579
|
+
},
|
|
580
|
+
getEvents() {
|
|
581
|
+
return getState().events
|
|
582
|
+
},
|
|
583
|
+
getEvent(id) {
|
|
584
|
+
return getState().events.find((event) => event.id === id)
|
|
585
|
+
},
|
|
586
|
+
setEvents(events) {
|
|
587
|
+
setField("events", events)
|
|
588
|
+
},
|
|
589
|
+
addEvent(event) {
|
|
590
|
+
setField("events", [...getState().events, event])
|
|
591
|
+
},
|
|
592
|
+
updateEvent(id, patch) {
|
|
593
|
+
const event = api.getEvent(id)
|
|
594
|
+
if (!event) return
|
|
595
|
+
const merged = { ...event, ...patch }
|
|
596
|
+
const timingChanged =
|
|
597
|
+
patch.start !== undefined ||
|
|
598
|
+
patch.end !== undefined ||
|
|
599
|
+
patch.allDay !== undefined
|
|
600
|
+
if (timingChanged && settings.onEventUpdate) {
|
|
601
|
+
// timing + rest commit as ONE events emission (a rejected update
|
|
602
|
+
// drops the whole patch, same as before)
|
|
603
|
+
const rest = { ...patch }
|
|
604
|
+
delete rest.start
|
|
605
|
+
delete rest.end
|
|
606
|
+
delete rest.allDay
|
|
607
|
+
applyProposedUpdate(
|
|
608
|
+
{
|
|
609
|
+
event: merged,
|
|
610
|
+
occurrence: null,
|
|
611
|
+
start: merged.start,
|
|
612
|
+
end: merged.end,
|
|
613
|
+
allDay: merged.allDay ?? false,
|
|
614
|
+
source: "api",
|
|
615
|
+
},
|
|
616
|
+
Object.keys(rest).length > 0 ? rest : undefined
|
|
617
|
+
)
|
|
618
|
+
return
|
|
619
|
+
}
|
|
620
|
+
setField(
|
|
621
|
+
"events",
|
|
622
|
+
getState().events.map((e) => (e.id === id ? merged : e))
|
|
623
|
+
)
|
|
624
|
+
},
|
|
625
|
+
removeEvent(id) {
|
|
626
|
+
setField(
|
|
627
|
+
"events",
|
|
628
|
+
getState().events.filter((event) => event.id !== id)
|
|
629
|
+
)
|
|
630
|
+
},
|
|
631
|
+
getOccurrences(range) {
|
|
632
|
+
if (!range) return getIndex().occurrences
|
|
633
|
+
const state = getState()
|
|
634
|
+
const within =
|
|
635
|
+
range.start >= state.visibleRange.start &&
|
|
636
|
+
range.end <= state.visibleRange.end
|
|
637
|
+
if (within) {
|
|
638
|
+
return getIndex().occurrences.filter((occ) => eventsOverlap(occ, range))
|
|
639
|
+
}
|
|
640
|
+
return buildEventIndex(state.events, range, {
|
|
641
|
+
timeZone: settings.timeZone,
|
|
642
|
+
eventOrder: settings.eventOrder,
|
|
643
|
+
getOccurrences: settings.getOccurrences,
|
|
644
|
+
}).occurrences
|
|
645
|
+
},
|
|
646
|
+
findOverlapping({ start, end, excludeEventId }) {
|
|
647
|
+
return api
|
|
648
|
+
.getOccurrences({ start, end })
|
|
649
|
+
.filter((occ) => occ.eventId !== excludeEventId)
|
|
650
|
+
},
|
|
651
|
+
select(partial) {
|
|
652
|
+
const current = getState().selection
|
|
653
|
+
setField("selection", {
|
|
654
|
+
eventKeys: partial.eventKeys ?? current.eventKeys,
|
|
655
|
+
slot: partial.slot !== undefined ? partial.slot : current.slot,
|
|
656
|
+
})
|
|
657
|
+
},
|
|
658
|
+
selectEvent(key, opts) {
|
|
659
|
+
const current = getState().selection
|
|
660
|
+
const eventKeys = opts?.additive
|
|
661
|
+
? current.eventKeys.includes(key)
|
|
662
|
+
? current.eventKeys.filter((k) => k !== key)
|
|
663
|
+
: [...current.eventKeys, key]
|
|
664
|
+
: [key]
|
|
665
|
+
setField("selection", { ...current, eventKeys })
|
|
666
|
+
},
|
|
667
|
+
clearSelection() {
|
|
668
|
+
setField("selection", EMPTY_SELECTION)
|
|
669
|
+
},
|
|
670
|
+
setInteractions(patch) {
|
|
671
|
+
setField("interactions", { ...getState().interactions, ...patch })
|
|
672
|
+
},
|
|
673
|
+
getVisibleRange() {
|
|
674
|
+
return getState().visibleRange
|
|
675
|
+
},
|
|
676
|
+
getActiveRange() {
|
|
677
|
+
return getState().activeRange
|
|
678
|
+
},
|
|
679
|
+
toZoned(date) {
|
|
680
|
+
return toZoned(date, settings.timeZone)
|
|
681
|
+
},
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const internals: GanttInternals<TData> = {
|
|
685
|
+
getIndex,
|
|
686
|
+
setDrag(drag) {
|
|
687
|
+
internal.drag = drag
|
|
688
|
+
invalidate()
|
|
689
|
+
notify()
|
|
690
|
+
},
|
|
691
|
+
setSlotDraft(draft) {
|
|
692
|
+
internal.slotDraft = draft
|
|
693
|
+
invalidate()
|
|
694
|
+
notify()
|
|
695
|
+
},
|
|
696
|
+
setViewportCenter(date) {
|
|
697
|
+
const prev = internal.viewportCenter
|
|
698
|
+
if (prev?.getTime() === date?.getTime()) return
|
|
699
|
+
internal.viewportCenter = date
|
|
700
|
+
invalidate()
|
|
701
|
+
notify()
|
|
702
|
+
},
|
|
703
|
+
applyProposedUpdate,
|
|
704
|
+
getSettingsVersion() {
|
|
705
|
+
return settingsVersion
|
|
706
|
+
},
|
|
707
|
+
extendRange(direction) {
|
|
708
|
+
const state = getState()
|
|
709
|
+
const bounds = settings.rangeBounds
|
|
710
|
+
if (
|
|
711
|
+
direction === "before" &&
|
|
712
|
+
bounds?.min &&
|
|
713
|
+
state.visibleRange.start.getTime() <= bounds.min.getTime()
|
|
714
|
+
) {
|
|
715
|
+
return false
|
|
716
|
+
}
|
|
717
|
+
if (
|
|
718
|
+
direction === "after" &&
|
|
719
|
+
bounds?.max &&
|
|
720
|
+
state.visibleRange.end.getTime() >= bounds.max.getTime()
|
|
721
|
+
) {
|
|
722
|
+
return false
|
|
723
|
+
}
|
|
724
|
+
const cap = Math.max(1, settings.maxRangeWindow ?? MAX_RANGE_WINDOW)
|
|
725
|
+
const { before, after } = internal.rangeWindow
|
|
726
|
+
const grow = direction === "before" ? before < cap : after < cap
|
|
727
|
+
if (grow) {
|
|
728
|
+
internal.rangeWindow =
|
|
729
|
+
direction === "before"
|
|
730
|
+
? { before: before + 1, after }
|
|
731
|
+
: { before, after: after + 1 }
|
|
732
|
+
} else {
|
|
733
|
+
// window is at capacity: SLIDE the anchor one period instead, so
|
|
734
|
+
// travel stays unbounded while the DOM stays bounded
|
|
735
|
+
const next = stepGanttDate(
|
|
736
|
+
state.scale,
|
|
737
|
+
state.date,
|
|
738
|
+
direction === "before" ? -1 : 1,
|
|
739
|
+
{
|
|
740
|
+
timeZone: settings.timeZone,
|
|
741
|
+
}
|
|
742
|
+
)
|
|
743
|
+
if (options.date !== undefined) {
|
|
744
|
+
// controlled anchor: propose the slide; nothing changes until the
|
|
745
|
+
// parent adopts it
|
|
746
|
+
settings.onDateChange?.(next)
|
|
747
|
+
return false
|
|
748
|
+
}
|
|
749
|
+
internal.date = next
|
|
750
|
+
lastAnchorChangeWasSlide = true
|
|
751
|
+
settings.onDateChange?.(next)
|
|
752
|
+
}
|
|
753
|
+
invalidate()
|
|
754
|
+
notify()
|
|
755
|
+
return true
|
|
756
|
+
},
|
|
757
|
+
didAnchorSlide() {
|
|
758
|
+
return lastAnchorChangeWasSlide
|
|
759
|
+
},
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const instance: GanttInstance<TData> = {
|
|
763
|
+
getState,
|
|
764
|
+
subscribe(listener) {
|
|
765
|
+
listeners.add(listener)
|
|
766
|
+
return () => listeners.delete(listener)
|
|
767
|
+
},
|
|
768
|
+
api,
|
|
769
|
+
get settings() {
|
|
770
|
+
return settings
|
|
771
|
+
},
|
|
772
|
+
internals,
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const STATE_KEYS = [
|
|
776
|
+
"events",
|
|
777
|
+
"scale",
|
|
778
|
+
"date",
|
|
779
|
+
"selection",
|
|
780
|
+
"interactions",
|
|
781
|
+
"loading",
|
|
782
|
+
] as const
|
|
783
|
+
const SETTINGS_KEYS = [
|
|
784
|
+
"timeZone",
|
|
785
|
+
"locale",
|
|
786
|
+
"weekStartsOn",
|
|
787
|
+
"slotDuration",
|
|
788
|
+
"snapDuration",
|
|
789
|
+
"i18n",
|
|
790
|
+
"rangeBounds",
|
|
791
|
+
"activation",
|
|
792
|
+
"maxRangeWindow",
|
|
793
|
+
"resources",
|
|
794
|
+
"overlap",
|
|
795
|
+
"enforceCanDrop",
|
|
796
|
+
"getEventPriority",
|
|
797
|
+
"eventOrder",
|
|
798
|
+
"getOccurrences",
|
|
799
|
+
] as const
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
instance,
|
|
803
|
+
setOptions(next) {
|
|
804
|
+
const prev = options
|
|
805
|
+
options = next
|
|
806
|
+
// compare by value: a freshly constructed but equal controlled date
|
|
807
|
+
// must not wipe infinite-scroll growth on every parent re-render
|
|
808
|
+
if (
|
|
809
|
+
prev.date?.getTime() !== next.date?.getTime() ||
|
|
810
|
+
prev.scale !== next.scale
|
|
811
|
+
) {
|
|
812
|
+
internal.rangeWindow = { before: 0, after: 0 }
|
|
813
|
+
lastAnchorChangeWasSlide = false
|
|
814
|
+
}
|
|
815
|
+
let changed = false
|
|
816
|
+
for (const key of STATE_KEYS) {
|
|
817
|
+
if (prev[key] !== next[key]) {
|
|
818
|
+
changed = true
|
|
819
|
+
break
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
let settingsChanged = false
|
|
823
|
+
for (const key of SETTINGS_KEYS) {
|
|
824
|
+
if (prev[key] !== next[key]) {
|
|
825
|
+
settingsChanged = true
|
|
826
|
+
break
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
settings = resolveSettings(next)
|
|
830
|
+
if (settingsChanged) {
|
|
831
|
+
settingsVersion++
|
|
832
|
+
changed = true
|
|
833
|
+
}
|
|
834
|
+
if (changed) invalidate()
|
|
835
|
+
return changed
|
|
836
|
+
},
|
|
837
|
+
notify,
|
|
838
|
+
emitRangeIfChanged,
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Headless root hook - the full calendar engine without any markup.
|
|
844
|
+
* Pass the returned instance to <Gantt calendar={instance}> or drive
|
|
845
|
+
* fully custom UI from instance.getState()/subscribe/api.
|
|
846
|
+
*/
|
|
847
|
+
function useGanttState<TData = unknown>(
|
|
848
|
+
options: UseGanttStateOptions<TData> = {}
|
|
849
|
+
): GanttInstance<TData> {
|
|
850
|
+
const [store] = useState(() => createGanttStore<TData>(options))
|
|
851
|
+
const changed = store.setOptions(options)
|
|
852
|
+
const changedRef = useRef(false)
|
|
853
|
+
if (changed) changedRef.current = true
|
|
854
|
+
useLayoutEffect(() => {
|
|
855
|
+
if (changedRef.current) {
|
|
856
|
+
changedRef.current = false
|
|
857
|
+
store.notify()
|
|
858
|
+
}
|
|
859
|
+
})
|
|
860
|
+
useEffect(() => {
|
|
861
|
+
store.emitRangeIfChanged()
|
|
862
|
+
// mount-only: onRangeChange fires once for the initial range
|
|
863
|
+
}, [])
|
|
864
|
+
return store.instance
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const GanttContext =
|
|
868
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
869
|
+
createContext<GanttInstance<any> | null>(null)
|
|
870
|
+
|
|
871
|
+
/** The stable calendar instance; throws outside <Gantt>. */
|
|
872
|
+
function useGantt<TData = unknown>(): GanttInstance<TData> {
|
|
873
|
+
const instance = useContext(GanttContext)
|
|
874
|
+
if (!instance) {
|
|
875
|
+
throw new Error("useGantt must be used within <Gantt>")
|
|
876
|
+
}
|
|
877
|
+
return instance as GanttInstance<TData>
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
interface UseGanttSelectorOptions<TData, TSelected> {
|
|
881
|
+
calendar?: GanttInstance<TData>
|
|
882
|
+
isEqual?: (a: TSelected, b: TSelected) => boolean
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** Fine-grained subscription with equality memoization (Object.is default). */
|
|
886
|
+
function useGanttSelector<TData = unknown, TSelected = unknown>(
|
|
887
|
+
selector: (state: GanttState<TData>) => TSelected,
|
|
888
|
+
options?: UseGanttSelectorOptions<TData, TSelected>
|
|
889
|
+
): TSelected {
|
|
890
|
+
const contextInstance = useContext(GanttContext)
|
|
891
|
+
const instance = options?.calendar ?? contextInstance
|
|
892
|
+
if (!instance) {
|
|
893
|
+
throw new Error(
|
|
894
|
+
"useGanttSelector needs an <Gantt> ancestor or an explicit `calendar` option"
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
const isEqual = options?.isEqual ?? Object.is
|
|
898
|
+
const lastRef = useRef<{ value: TSelected } | null>(null)
|
|
899
|
+
const selectorRef = useRef(selector)
|
|
900
|
+
selectorRef.current = selector
|
|
901
|
+
|
|
902
|
+
const getSnapshot = () => {
|
|
903
|
+
const next = selectorRef.current(instance.getState() as GanttState<TData>)
|
|
904
|
+
if (lastRef.current && isEqual(lastRef.current.value, next)) {
|
|
905
|
+
return lastRef.current.value
|
|
906
|
+
}
|
|
907
|
+
lastRef.current = { value: next }
|
|
908
|
+
return next
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
return useSyncExternalStore(instance.subscribe, getSnapshot, getSnapshot)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function useGanttScale(): {
|
|
915
|
+
scale: GanttScale
|
|
916
|
+
setScale: (scale: GanttScale) => void
|
|
917
|
+
} {
|
|
918
|
+
const instance = useGantt()
|
|
919
|
+
const scale = useGanttSelector((state) => state.scale)
|
|
920
|
+
return { scale, setScale: instance.api.setScale }
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function useGanttNavigation(): {
|
|
924
|
+
date: Date
|
|
925
|
+
/** i18n.functions.formatTitle output for the current view. */
|
|
926
|
+
title: string
|
|
927
|
+
visibleRange: GanttDateRange
|
|
928
|
+
activeRange: GanttDateRange
|
|
929
|
+
next: () => void
|
|
930
|
+
prev: () => void
|
|
931
|
+
today: () => void
|
|
932
|
+
goTo: (date: Date) => void
|
|
933
|
+
/** True when the anchor period contains now in the display time zone. */
|
|
934
|
+
isToday: boolean
|
|
935
|
+
} {
|
|
936
|
+
const instance = useGantt()
|
|
937
|
+
const { settings } = instance
|
|
938
|
+
const slice = useGanttSelector(
|
|
939
|
+
(state) => ({
|
|
940
|
+
date: state.date,
|
|
941
|
+
scale: state.scale,
|
|
942
|
+
visibleRange: state.visibleRange,
|
|
943
|
+
activeRange: state.activeRange,
|
|
944
|
+
viewportCenter: state.viewportCenter,
|
|
945
|
+
}),
|
|
946
|
+
{
|
|
947
|
+
isEqual: (a, b) =>
|
|
948
|
+
a.date.getTime() === b.date.getTime() &&
|
|
949
|
+
a.scale === b.scale &&
|
|
950
|
+
a.viewportCenter?.getTime() === b.viewportCenter?.getTime() &&
|
|
951
|
+
getRangeKey(a.visibleRange) === getRangeKey(b.visibleRange),
|
|
952
|
+
}
|
|
953
|
+
)
|
|
954
|
+
useGanttSettingsVersion(instance)
|
|
955
|
+
// Read in a selector, which runs on every render: compiled, a render-body
|
|
956
|
+
// clock read is cached and Today keeps its mount-time answer.
|
|
957
|
+
const isToday = useGanttSelector((state) => {
|
|
958
|
+
const now = new Date()
|
|
959
|
+
return now >= state.activeRange.start && now < state.activeRange.end
|
|
960
|
+
})
|
|
961
|
+
// The title names what you are LOOKING at: the visible-center period when
|
|
962
|
+
// the view reports one, otherwise the anchor period.
|
|
963
|
+
const titleDate = slice.viewportCenter ?? slice.date
|
|
964
|
+
const titleActive = slice.viewportCenter
|
|
965
|
+
? getGanttDateRange(slice.scale, slice.viewportCenter, {
|
|
966
|
+
timeZone: settings.timeZone,
|
|
967
|
+
weekStartsOn: settings.weekStartsOn,
|
|
968
|
+
}).activeRange
|
|
969
|
+
: slice.activeRange
|
|
970
|
+
return {
|
|
971
|
+
date: slice.date,
|
|
972
|
+
title: settings.i18n.functions.formatTitle(slice.scale, {
|
|
973
|
+
date: toZoned(titleDate, settings.timeZone),
|
|
974
|
+
activeRange: titleActive,
|
|
975
|
+
visibleRange: slice.visibleRange,
|
|
976
|
+
locale: settings.locale,
|
|
977
|
+
}),
|
|
978
|
+
visibleRange: slice.visibleRange,
|
|
979
|
+
activeRange: slice.activeRange,
|
|
980
|
+
next: instance.api.next,
|
|
981
|
+
prev: instance.api.prev,
|
|
982
|
+
today: instance.api.today,
|
|
983
|
+
goTo: instance.api.goTo,
|
|
984
|
+
isToday,
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function useGanttSelection(): {
|
|
989
|
+
selection: GanttSelection
|
|
990
|
+
select: (selection: Partial<GanttSelection>) => void
|
|
991
|
+
selectEvent: (key: string, opts?: { additive?: boolean }) => void
|
|
992
|
+
clearSelection: () => void
|
|
993
|
+
} {
|
|
994
|
+
const instance = useGantt()
|
|
995
|
+
const selection = useGanttSelector((state) => state.selection)
|
|
996
|
+
return {
|
|
997
|
+
selection,
|
|
998
|
+
select: instance.api.select,
|
|
999
|
+
selectEvent: instance.api.selectEvent,
|
|
1000
|
+
clearSelection: instance.api.clearSelection,
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function useGanttInteractions(): {
|
|
1005
|
+
interactions: GanttInteractions
|
|
1006
|
+
setInteractions: (patch: Partial<GanttInteractions>) => void
|
|
1007
|
+
} {
|
|
1008
|
+
const instance = useGantt()
|
|
1009
|
+
const interactions = useGanttSelector((state) => state.interactions)
|
|
1010
|
+
return { interactions, setInteractions: instance.api.setInteractions }
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/** Expanded, sorted occurrences; defaults to the visible range. */
|
|
1014
|
+
function useGanttOccurrences<TData = unknown>(
|
|
1015
|
+
range?: GanttDateRange
|
|
1016
|
+
): GanttOccurrence<TData>[] {
|
|
1017
|
+
const instance = useGantt<TData>()
|
|
1018
|
+
return useGanttSelector<TData, GanttOccurrence<TData>[]>(
|
|
1019
|
+
() => instance.api.getOccurrences(range),
|
|
1020
|
+
{
|
|
1021
|
+
calendar: instance,
|
|
1022
|
+
// keys encode id + start only, so end edits (resize-end) and payload
|
|
1023
|
+
// changes (title, color, progress) must be compared explicitly
|
|
1024
|
+
isEqual: (a, b) =>
|
|
1025
|
+
a.length === b.length &&
|
|
1026
|
+
a.every(
|
|
1027
|
+
(occ, i) =>
|
|
1028
|
+
occ.key === b[i]?.key &&
|
|
1029
|
+
occ.end.getTime() === b[i].end.getTime() &&
|
|
1030
|
+
occ.event === b[i].event
|
|
1031
|
+
),
|
|
1032
|
+
}
|
|
1033
|
+
)
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
interface GanttNodeSchedules<TData = unknown> {
|
|
1037
|
+
/** The node itself, or null when the id is not in the tree. */
|
|
1038
|
+
node: GanttResource | null
|
|
1039
|
+
/** Cardinality in force for this node (its own override, else the default). */
|
|
1040
|
+
scheduleMode: GanttScheduleMode
|
|
1041
|
+
/** The node's occurrences in the visible range, in axis order. */
|
|
1042
|
+
schedules: GanttOccurrence<TData>[]
|
|
1043
|
+
/** Pairs of the node's schedules that overlap in time. */
|
|
1044
|
+
conflicts: Array<[GanttOccurrence<TData>, GanttOccurrence<TData>]>
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Everything a consumer needs to MANAGE one node's schedules without
|
|
1049
|
+
* re-deriving layout: the node, its resolved cardinality, its schedules in
|
|
1050
|
+
* order, and the pairs that collide. Pure state - it renders nothing, so a
|
|
1051
|
+
* "manage schedules" panel is entirely the consumer's design.
|
|
1052
|
+
*/
|
|
1053
|
+
function useGanttNodeSchedules<TData = unknown>(
|
|
1054
|
+
nodeId: string
|
|
1055
|
+
): GanttNodeSchedules<TData> {
|
|
1056
|
+
const settings = useGanttSettings<TData>()
|
|
1057
|
+
const viewConfig = useGanttViewConfig<TData>()
|
|
1058
|
+
const occurrences = useGanttOccurrences<TData>()
|
|
1059
|
+
|
|
1060
|
+
const node = findResource(settings.resources, nodeId)
|
|
1061
|
+
const schedules = occurrences.filter(
|
|
1062
|
+
(occurrence) => occurrence.event.resourceId === nodeId
|
|
1063
|
+
)
|
|
1064
|
+
const conflicts: Array<[GanttOccurrence<TData>, GanttOccurrence<TData>]> = []
|
|
1065
|
+
for (let i = 0; i < schedules.length; i++) {
|
|
1066
|
+
for (let j = i + 1; j < schedules.length; j++) {
|
|
1067
|
+
if (eventsOverlap(schedules[i], schedules[j])) {
|
|
1068
|
+
conflicts.push([schedules[i], schedules[j]])
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
node,
|
|
1074
|
+
scheduleMode: resolveScheduleMode(node, viewConfig.scheduleMode),
|
|
1075
|
+
schedules,
|
|
1076
|
+
conflicts,
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** Subscribes to settings changes only (version counter, not state). */
|
|
1081
|
+
function useGanttSettingsVersion<TData>(
|
|
1082
|
+
instance: GanttInstance<TData>
|
|
1083
|
+
): number {
|
|
1084
|
+
return useSyncExternalStore(
|
|
1085
|
+
instance.subscribe,
|
|
1086
|
+
instance.internals.getSettingsVersion,
|
|
1087
|
+
instance.internals.getSettingsVersion
|
|
1088
|
+
)
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/** Resolved settings incl. merged i18n; re-renders only when settings change. */
|
|
1092
|
+
function useGanttSettings<TData = unknown>(): GanttSettings<TData> {
|
|
1093
|
+
const instance = useGantt<TData>()
|
|
1094
|
+
useGanttSettingsVersion(instance)
|
|
1095
|
+
return instance.settings
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
interface GanttClassNames {
|
|
1099
|
+
nav?: string
|
|
1100
|
+
toolbar?: string
|
|
1101
|
+
/** The gantt body (tree + track). */
|
|
1102
|
+
view?: string
|
|
1103
|
+
event?: string
|
|
1104
|
+
/** The planned-window (baseline) ghost drawn behind a bar. */
|
|
1105
|
+
baseline?: string
|
|
1106
|
+
/** The dependency-arrow layer (an SVG; color flows from currentColor). */
|
|
1107
|
+
dependencies?: string
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/** Row context handed to tree-panel column and label renderers. */
|
|
1111
|
+
interface GanttColumnContext {
|
|
1112
|
+
resource: GanttResource
|
|
1113
|
+
depth: number
|
|
1114
|
+
isGroup: boolean
|
|
1115
|
+
collapsed: boolean
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/** One extra tree-panel column after the built-in name column. */
|
|
1119
|
+
interface GanttColumn {
|
|
1120
|
+
/** Stable id; doubles as the default header label. */
|
|
1121
|
+
id: string
|
|
1122
|
+
/** Header label. */
|
|
1123
|
+
title?: ReactNode
|
|
1124
|
+
/** Fixed column width in px. Default 96. */
|
|
1125
|
+
width?: number
|
|
1126
|
+
/** Cell content alignment. Default "start". */
|
|
1127
|
+
align?: "start" | "center" | "end"
|
|
1128
|
+
/** Cell content per row; omit or return null for an empty cell. */
|
|
1129
|
+
render?: (ctx: GanttColumnContext) => ReactNode
|
|
1130
|
+
/** Extra classes on every cell of this column (header included). */
|
|
1131
|
+
className?: string
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/** Pointer-activation thresholds; unset keys keep the dnd-kit parity defaults. */
|
|
1135
|
+
interface GanttActivationConfig {
|
|
1136
|
+
/** Mouse travel (px) before a bar move starts. Default 5. */
|
|
1137
|
+
moveDistancePx?: number
|
|
1138
|
+
/** Mouse travel (px) before a drag-create starts. Default 4. */
|
|
1139
|
+
createDistancePx?: number
|
|
1140
|
+
/** Touch long-press delay in ms. Default 250. */
|
|
1141
|
+
touchDelayMs?: number
|
|
1142
|
+
/** Touch movement tolerance (px) during the long-press. Default 5. */
|
|
1143
|
+
touchTolerancePx?: number
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/** Layout metrics (rem unless noted); every knob falls back to its default. */
|
|
1147
|
+
/** A gridline: false to hide it, true for the default solid stroke, or a style. */
|
|
1148
|
+
type GanttGridLine = boolean | "solid" | "dashed"
|
|
1149
|
+
|
|
1150
|
+
interface GanttTimelineLines {
|
|
1151
|
+
/** Unit boundary lines running down the timeline. Default solid. */
|
|
1152
|
+
vertical?: GanttGridLine
|
|
1153
|
+
/** Row separator lines running across the timeline. Default solid. */
|
|
1154
|
+
horizontal?: GanttGridLine
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** Resolved stroke per axis; null means the axis draws nothing. */
|
|
1158
|
+
interface GanttResolvedLines {
|
|
1159
|
+
vertical: "solid" | "dashed" | null
|
|
1160
|
+
horizontal: "solid" | "dashed" | null
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* One place decides what the grid draws, so the header lines, the body lines
|
|
1165
|
+
* and the row separators can never disagree.
|
|
1166
|
+
*/
|
|
1167
|
+
function resolveTimelineLines(
|
|
1168
|
+
value: GanttTimelineLines | "vertical" | "both" | "none" | undefined
|
|
1169
|
+
): GanttResolvedLines {
|
|
1170
|
+
if (value === "none") return { vertical: null, horizontal: null }
|
|
1171
|
+
if (value === "vertical") return { vertical: "solid", horizontal: null }
|
|
1172
|
+
if (value === "both" || value === undefined) {
|
|
1173
|
+
return { vertical: "solid", horizontal: "solid" }
|
|
1174
|
+
}
|
|
1175
|
+
const stroke = (line: GanttGridLine | undefined) =>
|
|
1176
|
+
line === false ? null : line === true || line === undefined ? "solid" : line
|
|
1177
|
+
return {
|
|
1178
|
+
vertical: stroke(value.vertical),
|
|
1179
|
+
horizontal: stroke(value.horizontal),
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
interface GanttMetrics {
|
|
1184
|
+
/** Height of one schedule bar. Default 1.25. */
|
|
1185
|
+
laneHeight?: number
|
|
1186
|
+
/** Gap between stacked schedules in one node. Default 0.1875. */
|
|
1187
|
+
laneGap?: number
|
|
1188
|
+
/**
|
|
1189
|
+
* Vertical inset between the row's edges and its block of schedules - the
|
|
1190
|
+
* breathing room around the stack, kept separate from laneGap so schedules
|
|
1191
|
+
* in one node can sit tight without cramping the row. Default 0.5.
|
|
1192
|
+
*/
|
|
1193
|
+
rowPadding?: number
|
|
1194
|
+
/** Minimum row height. Default 2.5. */
|
|
1195
|
+
minRowHeight?: number
|
|
1196
|
+
/**
|
|
1197
|
+
* Drag-drop indicator height, centered in its lane band and published on
|
|
1198
|
+
* the ghost as --gantt-ghost-height so consumer styling can read the same
|
|
1199
|
+
* number. Default 1.25.
|
|
1200
|
+
*/
|
|
1201
|
+
ghostHeight?: number
|
|
1202
|
+
/** barLabel "auto" flips the title outside below this bar width. Default 7. */
|
|
1203
|
+
autoLabelMin?: number
|
|
1204
|
+
/** Unit width at zoom 1, per scale. Day scale = width per interval unit. */
|
|
1205
|
+
unitWidths?: Partial<Record<GanttScale, number>>
|
|
1206
|
+
/** Minimum timeline pane width in px. Default 200. */
|
|
1207
|
+
minTimelineWidth?: number
|
|
1208
|
+
/** Scroll distance (px) from an edge that grows the range. Default 160. */
|
|
1209
|
+
infiniteScrollEdge?: number
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/** Live gesture snapshot handed to the drag/resize indicator render props. */
|
|
1213
|
+
interface GanttDragIndicatorProps<TData = unknown> {
|
|
1214
|
+
occurrence: GanttOccurrence<TData>
|
|
1215
|
+
kind: "move" | "resize-start" | "resize-end"
|
|
1216
|
+
/** Proposed (snapped) range of the current gesture step. */
|
|
1217
|
+
start: Date
|
|
1218
|
+
end: Date
|
|
1219
|
+
valid: boolean
|
|
1220
|
+
/** The event's planned (baseline) window, so a custom overlay can keep
|
|
1221
|
+
* drawing it mid-gesture; null when the event carries none. */
|
|
1222
|
+
baseline: GanttBaseline | null
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/** Slot handed to a custom schedule-hint renderer. */
|
|
1226
|
+
interface GanttScheduleHintProps {
|
|
1227
|
+
start: Date
|
|
1228
|
+
end: Date
|
|
1229
|
+
resource: GanttResource
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/** Parent rollup handed to a custom summary renderer. */
|
|
1233
|
+
interface GanttSummaryProps {
|
|
1234
|
+
resource: GanttResource
|
|
1235
|
+
start: Date
|
|
1236
|
+
end: Date
|
|
1237
|
+
progress: number | null
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/** Planned window handed to a custom baseline renderer. */
|
|
1241
|
+
interface GanttBaselineProps<TData = unknown> {
|
|
1242
|
+
event: GanttEvent<TData>
|
|
1243
|
+
start: Date
|
|
1244
|
+
end: Date
|
|
1245
|
+
/** Planned start and end coincide: a point in the plan, not a window. */
|
|
1246
|
+
milestone: boolean
|
|
1247
|
+
variance: GanttBaselineVariance
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/** Planned NODE window handed to a custom row-baseline renderer. */
|
|
1251
|
+
interface GanttRowBaselineProps {
|
|
1252
|
+
resource: GanttResource
|
|
1253
|
+
start: Date
|
|
1254
|
+
end: Date
|
|
1255
|
+
/** Planned start and end coincide: a point in the plan, not a window. */
|
|
1256
|
+
milestone: boolean
|
|
1257
|
+
/** Latest actual end across the node's (and, for a group, its subtree's)
|
|
1258
|
+
* events against the planned end; null when there is nothing to compare. */
|
|
1259
|
+
variance: GanttBaselineVariance | null
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/** Left tree-panel sizing and splitter behavior. */
|
|
1263
|
+
interface GanttTreePanelConfig {
|
|
1264
|
+
/** Initial panel width in px. Default 288. */
|
|
1265
|
+
width?: number
|
|
1266
|
+
/** Splitter lower bound in px. Default 180. */
|
|
1267
|
+
minWidth?: number
|
|
1268
|
+
/** Splitter upper bound in px. Default 640. */
|
|
1269
|
+
maxWidth?: number
|
|
1270
|
+
/** Drag/keyboard splitter between the panels. Default true. */
|
|
1271
|
+
resizable?: boolean
|
|
1272
|
+
/** Width of the sticky name column in px. Default 208. */
|
|
1273
|
+
nameColumnWidth?: number
|
|
1274
|
+
/** Fires after any user resize (drag release, keyboard, double-click reset). */
|
|
1275
|
+
onWidthChange?: (width: number) => void
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
interface GanttRenderEventProps<TData = unknown> {
|
|
1279
|
+
occurrence: GanttOccurrence<TData>
|
|
1280
|
+
segment: GanttSegment<TData>
|
|
1281
|
+
isDragging: boolean
|
|
1282
|
+
isSelected: boolean
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* View-layer configuration: display props and render overrides. These live on
|
|
1287
|
+
* <Gantt> (and per-view components), never in the headless options.
|
|
1288
|
+
*/
|
|
1289
|
+
interface GanttViewConfig<TData = unknown> {
|
|
1290
|
+
/** Red now-line on the axis. */
|
|
1291
|
+
nowIndicator: boolean
|
|
1292
|
+
/**
|
|
1293
|
+
* Day-scale unit interval in minutes: axis units and gridlines follow it.
|
|
1294
|
+
*/
|
|
1295
|
+
interval: number
|
|
1296
|
+
/**
|
|
1297
|
+
* Scroll implementation for the gantt body: "custom" (default, shadcn
|
|
1298
|
+
* ScrollArea) or "native" (browser scrollbars via overflow auto).
|
|
1299
|
+
*/
|
|
1300
|
+
scrollbars: "custom" | "native"
|
|
1301
|
+
/**
|
|
1302
|
+
* Placement hint over empty timeline track: a validated, snapped tile that
|
|
1303
|
+
* opens the schedule flow (onSlotClick, else onSelectSlot) at that day.
|
|
1304
|
+
* Works on every scale. Default off.
|
|
1305
|
+
*/
|
|
1306
|
+
displayScheduleHint: boolean
|
|
1307
|
+
/**
|
|
1308
|
+
* Where the viewport opens. `"now"` (default) centres the current instant
|
|
1309
|
+
* when the anchor period contains it and falls back to the anchor; `"anchor"`
|
|
1310
|
+
* always centres the anchor; a Date centres that instant.
|
|
1311
|
+
*
|
|
1312
|
+
* Only `"now"` follows the wall clock - which is right for a live board and
|
|
1313
|
+
* wrong for a demo or a report, whose opening composition must not depend on
|
|
1314
|
+
* the hour it is viewed at. Those pass an explicit instant.
|
|
1315
|
+
*/
|
|
1316
|
+
initialCenter: "now" | "anchor" | Date
|
|
1317
|
+
/**
|
|
1318
|
+
* Empty-track presses on schedulable rows start a drag-create gesture that
|
|
1319
|
+
* commits through onSelectSlot. Default off: the whole panel drags-to-pan
|
|
1320
|
+
* instead, and scheduling flows through the hint tile / onSlotClick.
|
|
1321
|
+
*/
|
|
1322
|
+
dragCreate: boolean
|
|
1323
|
+
/**
|
|
1324
|
+
* "Add task" affordance at the foot of the tree that opens the create-task
|
|
1325
|
+
* flow (onCreateTask). Shown only when canCreateTask allows it. Default off.
|
|
1326
|
+
*/
|
|
1327
|
+
displayCreateTaskHint: boolean
|
|
1328
|
+
/** Floating zoom in/out control over the track. Default on. */
|
|
1329
|
+
zoomControl: boolean
|
|
1330
|
+
/**
|
|
1331
|
+
* Ctrl/Cmd + wheel over the timeline zooms the time range, anchored on the
|
|
1332
|
+
* pointer. Trackpad pinch arrives as the same event (browsers set ctrlKey
|
|
1333
|
+
* on it), so this is also the pinch-to-zoom switch. Default on. The gesture
|
|
1334
|
+
* is handed back to the browser at the zoom limits, so page zoom still
|
|
1335
|
+
* works there.
|
|
1336
|
+
*/
|
|
1337
|
+
wheelZoom: boolean
|
|
1338
|
+
/** Nav button variant; all nav buttons follow it. Default "ghost". */
|
|
1339
|
+
navButtonVariant: "ghost" | "outline" | "secondary" | "default"
|
|
1340
|
+
/** Nav button size; icon buttons use the icon twin. Default "sm". */
|
|
1341
|
+
navButtonSize: "sm" | "default"
|
|
1342
|
+
/**
|
|
1343
|
+
* Off-day (non-working day) marking on day/week/month scales. true =
|
|
1344
|
+
* weekends with a muted background; a config object customizes weekdays,
|
|
1345
|
+
* explicit dates, a predicate, and the marker class.
|
|
1346
|
+
*/
|
|
1347
|
+
offDays?: boolean | GanttOffDaysConfig
|
|
1348
|
+
/**
|
|
1349
|
+
* Extra tree-panel columns after the built-in name column. The tree panel
|
|
1350
|
+
* scrolls horizontally when the columns outgrow it; the name column stays
|
|
1351
|
+
* pinned.
|
|
1352
|
+
*/
|
|
1353
|
+
columns?: GanttColumn[]
|
|
1354
|
+
/**
|
|
1355
|
+
* Consumer slot pinned at the end of the tree-panel header - the intended
|
|
1356
|
+
* home for an add/remove-columns dropdown menu.
|
|
1357
|
+
*/
|
|
1358
|
+
columnsMenu?: ReactNode
|
|
1359
|
+
/** Tree-panel width, splitter bounds, and resizability. */
|
|
1360
|
+
treePanel?: GanttTreePanelConfig
|
|
1361
|
+
/**
|
|
1362
|
+
* Timeline gridlines. The object form controls the two axes independently
|
|
1363
|
+
* and gives each its own stroke: `{ vertical: "dashed", horizontal: true }`.
|
|
1364
|
+
* An omitted axis stays on and solid. `true` means solid.
|
|
1365
|
+
*
|
|
1366
|
+
* The three legacy shorthands still work: "none" (bare), "vertical" (unit
|
|
1367
|
+
* boundaries only, rows separated by whitespace) and "both" (adds row
|
|
1368
|
+
* separators).
|
|
1369
|
+
*/
|
|
1370
|
+
timelineLines: GanttTimelineLines | "vertical" | "both" | "none"
|
|
1371
|
+
/**
|
|
1372
|
+
* Bar title placement: "inside" (default) renders it in the bar, "outside"
|
|
1373
|
+
* beside the bar, "auto" moves it outside only when the bar is too short.
|
|
1374
|
+
*/
|
|
1375
|
+
barLabel: "inside" | "outside" | "auto"
|
|
1376
|
+
/** Edge chips that scroll to bars outside the visible timeline. Default true. */
|
|
1377
|
+
offscreenIndicators: boolean
|
|
1378
|
+
/**
|
|
1379
|
+
* Extend the timeline into the past/future while scrolling near an edge
|
|
1380
|
+
* (the anchor period stays the nav title). Default true.
|
|
1381
|
+
*/
|
|
1382
|
+
infiniteScroll: boolean
|
|
1383
|
+
/** Zoom bounds and button step for the floating control. Default 0.5 - 3, step 0.25. */
|
|
1384
|
+
zoomRange?: { min?: number; max?: number; step?: number }
|
|
1385
|
+
/** Layout metric overrides (row/lane/unit geometry, thresholds). */
|
|
1386
|
+
metrics?: GanttMetrics
|
|
1387
|
+
/** Sticky nav bar (same contract as the event calendar). Default false. */
|
|
1388
|
+
stickyNav: boolean
|
|
1389
|
+
/**
|
|
1390
|
+
* Leaf-row selection checkboxes in the tree panel. Default true;
|
|
1391
|
+
* uncontrolled unless selectedRows is passed.
|
|
1392
|
+
*/
|
|
1393
|
+
rowCheckboxes: boolean
|
|
1394
|
+
/** Controlled selected row ids; pairs with onSelectedRowsChange. */
|
|
1395
|
+
selectedRows?: string[]
|
|
1396
|
+
onSelectedRowsChange?: (ids: string[]) => void
|
|
1397
|
+
/** Controlled collapsed group ids; pairs with onCollapsedGroupsChange. */
|
|
1398
|
+
collapsedGroups?: string[]
|
|
1399
|
+
/** Initial collapsed group ids (uncontrolled). */
|
|
1400
|
+
defaultCollapsedGroups?: string[]
|
|
1401
|
+
onCollapsedGroupsChange?: (ids: string[]) => void
|
|
1402
|
+
/** Controlled zoom multiplier; pairs with onZoomChange. */
|
|
1403
|
+
zoom?: number
|
|
1404
|
+
/** Initial zoom multiplier (uncontrolled). Default 1. */
|
|
1405
|
+
defaultZoom?: number
|
|
1406
|
+
onZoomChange?: (zoom: number) => void
|
|
1407
|
+
/**
|
|
1408
|
+
* Allow drag-create and slot clicks on rows that have children. Default
|
|
1409
|
+
* false: parents aggregate their subtree instead of owning bars.
|
|
1410
|
+
*/
|
|
1411
|
+
parentScheduling: boolean
|
|
1412
|
+
/**
|
|
1413
|
+
* Rollup strips on parent rows without bars of their own: the envelope of
|
|
1414
|
+
* descendant bars with duration-weighted progress. Default true.
|
|
1415
|
+
*/
|
|
1416
|
+
summaryBars: boolean
|
|
1417
|
+
/**
|
|
1418
|
+
* As-built comparison: the planned window of every bar carrying
|
|
1419
|
+
* baselineStart/baselineEnd, ghosted behind it on its own lane (equal
|
|
1420
|
+
* instants draw a planned milestone diamond), and of every NODE whose
|
|
1421
|
+
* GanttResource carries the same pair, drawn as a band behind its lanes.
|
|
1422
|
+
* Display only - dragging a bar moves the actual dates and never its
|
|
1423
|
+
* baseline. Default true; rows and events without baseline data draw
|
|
1424
|
+
* nothing.
|
|
1425
|
+
*/
|
|
1426
|
+
baselineBars: boolean
|
|
1427
|
+
/**
|
|
1428
|
+
* Finish-to-start arrows between bars whose events name `dependencies`.
|
|
1429
|
+
* Drawn from the predecessor's end into the dependent's start, under the
|
|
1430
|
+
* bars; endpoints on hidden rows are skipped. Default true; nothing
|
|
1431
|
+
* renders when no visible event names a dependency.
|
|
1432
|
+
*/
|
|
1433
|
+
dependencyLines: boolean
|
|
1434
|
+
/**
|
|
1435
|
+
* How many schedules a tree node may hold. "multiple" (default) stacks
|
|
1436
|
+
* concurrent schedules into stable lanes and grows the row; "single" keeps
|
|
1437
|
+
* one track per node - the task-gantt shape. Any node can override it with
|
|
1438
|
+
* its own `scheduleMode`.
|
|
1439
|
+
*/
|
|
1440
|
+
scheduleMode: GanttScheduleMode
|
|
1441
|
+
/**
|
|
1442
|
+
* Vertical placement of a row's content once a node holds several lanes.
|
|
1443
|
+
* "start" (default) keeps the tree label on the baseline of the FIRST
|
|
1444
|
+
* schedule; "center" centers both against the grown row.
|
|
1445
|
+
*/
|
|
1446
|
+
rowAlign: GanttRowAlign
|
|
1447
|
+
classNames?: GanttClassNames
|
|
1448
|
+
renderEvent?: (props: GanttRenderEventProps<TData>) => ReactNode
|
|
1449
|
+
/**
|
|
1450
|
+
* Right-click menu for a bar: return shadcn ContextMenu items (the primitive
|
|
1451
|
+
* wraps every bar in a ContextMenu and renders this as its content). Read
|
|
1452
|
+
* the occurrence for the subject and drive actions through the gantt api
|
|
1453
|
+
* (useGantt) or your own state - fully headless. Omit for no menu.
|
|
1454
|
+
*/
|
|
1455
|
+
renderEventMenu?: (props: GanttRenderEventProps<TData>) => ReactNode
|
|
1456
|
+
/**
|
|
1457
|
+
* Tree-node label. Receives the resource with its tree position; return
|
|
1458
|
+
* any rich content (icons, badges). Default is the plain title.
|
|
1459
|
+
*/
|
|
1460
|
+
renderResourceLabel?: (props: {
|
|
1461
|
+
resource: GanttResource
|
|
1462
|
+
depth: number
|
|
1463
|
+
isGroup: boolean
|
|
1464
|
+
collapsed: boolean
|
|
1465
|
+
}) => ReactNode
|
|
1466
|
+
/**
|
|
1467
|
+
* Right-click menu for a tree row (same contract as renderEventMenu):
|
|
1468
|
+
* return shadcn ContextMenu items and drive actions through your own state.
|
|
1469
|
+
*/
|
|
1470
|
+
renderResourceMenu?: (ctx: GanttColumnContext) => ReactNode
|
|
1471
|
+
/** Rendered in the timeline body when there are no resources. */
|
|
1472
|
+
renderNoResources?: () => ReactNode
|
|
1473
|
+
/**
|
|
1474
|
+
* Replaces the smooth cursor-following MOVE clone. Content is React and
|
|
1475
|
+
* re-renders per snap step; the gantt owns the fixed wrapper and writes
|
|
1476
|
+
* its position imperatively per pointermove (no per-frame React).
|
|
1477
|
+
*/
|
|
1478
|
+
renderDragPreview?: (props: GanttDragIndicatorProps<TData>) => ReactNode
|
|
1479
|
+
/**
|
|
1480
|
+
* Replaces the RESIZE edge line + status chip. Same positioning contract
|
|
1481
|
+
* as renderDragPreview: your content, gantt-owned cursor tracking.
|
|
1482
|
+
*/
|
|
1483
|
+
renderResizeIndicator?: (props: GanttDragIndicatorProps<TData>) => ReactNode
|
|
1484
|
+
/**
|
|
1485
|
+
* Replaces the schedule-hint tile + bubble. Rendered inside the snapped,
|
|
1486
|
+
* validated, pointer-transparent wrapper: set pointer-events-auto on your
|
|
1487
|
+
* clickable parts and drive your own create flow from the slot.
|
|
1488
|
+
*/
|
|
1489
|
+
renderScheduleHint?: (props: GanttScheduleHintProps) => ReactNode
|
|
1490
|
+
/** Replaces the parent rollup strip (the positioned wrapper stays gantt-owned). */
|
|
1491
|
+
renderSummary?: (props: GanttSummaryProps) => ReactNode
|
|
1492
|
+
/**
|
|
1493
|
+
* Replaces the baseline ghost's content, the milestone diamond included
|
|
1494
|
+
* (props.milestone forks the two). The positioned, pointer-transparent
|
|
1495
|
+
* wrapper stays gantt-owned.
|
|
1496
|
+
*/
|
|
1497
|
+
renderBaseline?: (props: GanttBaselineProps<TData>) => ReactNode
|
|
1498
|
+
/**
|
|
1499
|
+
* Replaces the ROW baseline band's content (same wrapper contract as
|
|
1500
|
+
* renderBaseline).
|
|
1501
|
+
*/
|
|
1502
|
+
renderRowBaseline?: (props: GanttRowBaselineProps) => ReactNode
|
|
1503
|
+
/**
|
|
1504
|
+
* Replaces the rollup MATH: return 0-100 (or null to hide) for a group
|
|
1505
|
+
* from its descendant events. Default: duration-weighted mean progress.
|
|
1506
|
+
*/
|
|
1507
|
+
getSummaryProgress?: (ctx: {
|
|
1508
|
+
resource: GanttResource
|
|
1509
|
+
events: GanttEvent<TData>[]
|
|
1510
|
+
}) => number | null
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const DEFAULT_VIEW_CONFIG: GanttViewConfig = {
|
|
1514
|
+
nowIndicator: true,
|
|
1515
|
+
interval: 60,
|
|
1516
|
+
scrollbars: "custom",
|
|
1517
|
+
displayScheduleHint: false,
|
|
1518
|
+
initialCenter: "now",
|
|
1519
|
+
displayCreateTaskHint: false,
|
|
1520
|
+
dragCreate: false,
|
|
1521
|
+
zoomControl: true,
|
|
1522
|
+
wheelZoom: true,
|
|
1523
|
+
navButtonVariant: "ghost",
|
|
1524
|
+
navButtonSize: "sm",
|
|
1525
|
+
timelineLines: "vertical",
|
|
1526
|
+
barLabel: "inside",
|
|
1527
|
+
offscreenIndicators: true,
|
|
1528
|
+
infiniteScroll: true,
|
|
1529
|
+
stickyNav: false,
|
|
1530
|
+
rowCheckboxes: true,
|
|
1531
|
+
parentScheduling: false,
|
|
1532
|
+
summaryBars: true,
|
|
1533
|
+
baselineBars: true,
|
|
1534
|
+
dependencyLines: true,
|
|
1535
|
+
scheduleMode: "multiple",
|
|
1536
|
+
rowAlign: "start",
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const GanttViewConfigContext = createContext<
|
|
1540
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1541
|
+
GanttViewConfig<any>
|
|
1542
|
+
>(DEFAULT_VIEW_CONFIG)
|
|
1543
|
+
|
|
1544
|
+
/** Root-level display props + render overrides, for view components. */
|
|
1545
|
+
function useGanttViewConfig<TData = unknown>(): GanttViewConfig<TData> {
|
|
1546
|
+
return useContext(GanttViewConfigContext)
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const VIEW_CONFIG_KEYS: Array<keyof GanttViewConfig> = [
|
|
1550
|
+
"nowIndicator",
|
|
1551
|
+
"interval",
|
|
1552
|
+
"scrollbars",
|
|
1553
|
+
"displayScheduleHint",
|
|
1554
|
+
"initialCenter",
|
|
1555
|
+
"displayCreateTaskHint",
|
|
1556
|
+
"dragCreate",
|
|
1557
|
+
"zoomControl",
|
|
1558
|
+
"wheelZoom",
|
|
1559
|
+
"navButtonVariant",
|
|
1560
|
+
"navButtonSize",
|
|
1561
|
+
"offDays",
|
|
1562
|
+
"columns",
|
|
1563
|
+
"columnsMenu",
|
|
1564
|
+
"treePanel",
|
|
1565
|
+
"metrics",
|
|
1566
|
+
"timelineLines",
|
|
1567
|
+
"barLabel",
|
|
1568
|
+
"offscreenIndicators",
|
|
1569
|
+
"infiniteScroll",
|
|
1570
|
+
"zoomRange",
|
|
1571
|
+
"stickyNav",
|
|
1572
|
+
"rowCheckboxes",
|
|
1573
|
+
"selectedRows",
|
|
1574
|
+
"onSelectedRowsChange",
|
|
1575
|
+
"collapsedGroups",
|
|
1576
|
+
"defaultCollapsedGroups",
|
|
1577
|
+
"onCollapsedGroupsChange",
|
|
1578
|
+
"zoom",
|
|
1579
|
+
"defaultZoom",
|
|
1580
|
+
"onZoomChange",
|
|
1581
|
+
"parentScheduling",
|
|
1582
|
+
"summaryBars",
|
|
1583
|
+
"baselineBars",
|
|
1584
|
+
"dependencyLines",
|
|
1585
|
+
"scheduleMode",
|
|
1586
|
+
"rowAlign",
|
|
1587
|
+
"classNames",
|
|
1588
|
+
"renderEvent",
|
|
1589
|
+
"renderEventMenu",
|
|
1590
|
+
"renderResourceLabel",
|
|
1591
|
+
"renderResourceMenu",
|
|
1592
|
+
"renderNoResources",
|
|
1593
|
+
"renderDragPreview",
|
|
1594
|
+
"renderResizeIndicator",
|
|
1595
|
+
"renderScheduleHint",
|
|
1596
|
+
"renderSummary",
|
|
1597
|
+
"renderBaseline",
|
|
1598
|
+
"renderRowBaseline",
|
|
1599
|
+
"getSummaryProgress",
|
|
1600
|
+
]
|
|
1601
|
+
|
|
1602
|
+
interface GanttProps<TData = unknown>
|
|
1603
|
+
extends
|
|
1604
|
+
UseGanttStateOptions<TData>,
|
|
1605
|
+
Partial<GanttViewConfig<TData>>,
|
|
1606
|
+
Omit<useRender.ComponentProps<"div">, "children" | "defaultValue"> {
|
|
1607
|
+
/** Adopt a hoisted useGanttState instance; option props are then ignored. */
|
|
1608
|
+
calendar?: GanttInstance<TData>
|
|
1609
|
+
/** Imperative escape hatch usable from outside the tree. */
|
|
1610
|
+
apiRef?: RefObject<GanttApi<TData> | null>
|
|
1611
|
+
children?: ReactNode
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const OPTION_KEYS: Array<keyof UseGanttStateOptions> = [
|
|
1615
|
+
"events",
|
|
1616
|
+
"defaultEvents",
|
|
1617
|
+
"scale",
|
|
1618
|
+
"defaultScale",
|
|
1619
|
+
"date",
|
|
1620
|
+
"defaultDate",
|
|
1621
|
+
"selection",
|
|
1622
|
+
"defaultSelection",
|
|
1623
|
+
"interactions",
|
|
1624
|
+
"defaultInteractions",
|
|
1625
|
+
"loading",
|
|
1626
|
+
"timeZone",
|
|
1627
|
+
"locale",
|
|
1628
|
+
"weekStartsOn",
|
|
1629
|
+
"slotDuration",
|
|
1630
|
+
"snapDuration",
|
|
1631
|
+
"i18n",
|
|
1632
|
+
"rangeBounds",
|
|
1633
|
+
"activation",
|
|
1634
|
+
"maxRangeWindow",
|
|
1635
|
+
"resources",
|
|
1636
|
+
"overlap",
|
|
1637
|
+
"enforceCanDrop",
|
|
1638
|
+
"getEventPriority",
|
|
1639
|
+
"eventOrder",
|
|
1640
|
+
"getOccurrences",
|
|
1641
|
+
"onEventClick",
|
|
1642
|
+
"onEventDoubleClick",
|
|
1643
|
+
"onEventUpdate",
|
|
1644
|
+
"canDropEvent",
|
|
1645
|
+
"onSlotClick",
|
|
1646
|
+
"onSelectSlot",
|
|
1647
|
+
"canSelectSlot",
|
|
1648
|
+
"onCreateTask",
|
|
1649
|
+
"canCreateTask",
|
|
1650
|
+
"onResourceClick",
|
|
1651
|
+
"onResourceDoubleClick",
|
|
1652
|
+
"onRangeChange",
|
|
1653
|
+
"onScaleChange",
|
|
1654
|
+
"onDateChange",
|
|
1655
|
+
"onSelectionChange",
|
|
1656
|
+
"onInteractionsChange",
|
|
1657
|
+
"onEventsChange",
|
|
1658
|
+
"onResourceReorder",
|
|
1659
|
+
"onResourceReorderReject",
|
|
1660
|
+
"canReorderResource",
|
|
1661
|
+
]
|
|
1662
|
+
|
|
1663
|
+
function shallowEqualRecord(
|
|
1664
|
+
a: Record<string, unknown>,
|
|
1665
|
+
b: Record<string, unknown>
|
|
1666
|
+
): boolean {
|
|
1667
|
+
const aKeys = Object.keys(a)
|
|
1668
|
+
if (aKeys.length !== Object.keys(b).length) return false
|
|
1669
|
+
for (const key of aKeys) {
|
|
1670
|
+
if (!Object.is(a[key], b[key])) return false
|
|
1671
|
+
}
|
|
1672
|
+
return true
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function splitOptions<TData>(props: Record<string, unknown>): {
|
|
1676
|
+
options: UseGanttStateOptions<TData>
|
|
1677
|
+
viewConfig: GanttViewConfig<TData>
|
|
1678
|
+
rest: Record<string, unknown>
|
|
1679
|
+
} {
|
|
1680
|
+
const options: Record<string, unknown> = {}
|
|
1681
|
+
const viewConfig: Record<string, unknown> = { ...DEFAULT_VIEW_CONFIG }
|
|
1682
|
+
const rest: Record<string, unknown> = {}
|
|
1683
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1684
|
+
if ((OPTION_KEYS as string[]).includes(key)) options[key] = value
|
|
1685
|
+
else if ((VIEW_CONFIG_KEYS as string[]).includes(key)) {
|
|
1686
|
+
if (value !== undefined) viewConfig[key] = value
|
|
1687
|
+
} else rest[key] = value
|
|
1688
|
+
}
|
|
1689
|
+
return {
|
|
1690
|
+
options: options as UseGanttStateOptions<TData>,
|
|
1691
|
+
viewConfig: viewConfig as unknown as GanttViewConfig<TData>,
|
|
1692
|
+
rest,
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* Root provider + container. Composition contract:
|
|
1698
|
+
* <Gantt><GanttNav/><GanttToolbar/><GanttView/></Gantt>
|
|
1699
|
+
*/
|
|
1700
|
+
function Gantt<TData = unknown>({
|
|
1701
|
+
calendar,
|
|
1702
|
+
apiRef,
|
|
1703
|
+
className,
|
|
1704
|
+
render,
|
|
1705
|
+
children,
|
|
1706
|
+
...props
|
|
1707
|
+
}: GanttProps<TData>) {
|
|
1708
|
+
const { options, viewConfig, rest } = splitOptions<TData>(
|
|
1709
|
+
props as Record<string, unknown>
|
|
1710
|
+
)
|
|
1711
|
+
|
|
1712
|
+
// Stable context identity: splitOptions builds a fresh object per render,
|
|
1713
|
+
// and every row subscribes to this context - hand out the previous object
|
|
1714
|
+
// unless a config value actually changed.
|
|
1715
|
+
const viewConfigRef = useRef(viewConfig)
|
|
1716
|
+
if (
|
|
1717
|
+
!shallowEqualRecord(
|
|
1718
|
+
viewConfigRef.current as unknown as Record<string, unknown>,
|
|
1719
|
+
viewConfig as unknown as Record<string, unknown>
|
|
1720
|
+
)
|
|
1721
|
+
) {
|
|
1722
|
+
viewConfigRef.current = viewConfig
|
|
1723
|
+
}
|
|
1724
|
+
const stableViewConfig = viewConfigRef.current
|
|
1725
|
+
|
|
1726
|
+
if (calendar && Object.keys(options).length > 0) {
|
|
1727
|
+
warnOnce(
|
|
1728
|
+
"calendar-and-options",
|
|
1729
|
+
"both `calendar` and option props were passed; option props are ignored when adopting an instance."
|
|
1730
|
+
)
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
const own = useGanttState<TData>(calendar ? {} : options)
|
|
1734
|
+
const instance = calendar ?? own
|
|
1735
|
+
|
|
1736
|
+
useEffect(() => {
|
|
1737
|
+
if (apiRef) apiRef.current = instance.api
|
|
1738
|
+
}, [apiRef, instance])
|
|
1739
|
+
|
|
1740
|
+
const defaultProps = {
|
|
1741
|
+
"data-slot": "gantt",
|
|
1742
|
+
// own the foreground (previews and consumer shells may not set body
|
|
1743
|
+
// color) and the type scale: every gantt label inherits the root's text
|
|
1744
|
+
// size, so one class here (or on the consumer's className) rescales the
|
|
1745
|
+
// whole component - e.g. className="text-sm" for a roomier grid
|
|
1746
|
+
className: cn(
|
|
1747
|
+
"text-foreground flex min-h-0 min-w-0 flex-col text-xs",
|
|
1748
|
+
className
|
|
1749
|
+
),
|
|
1750
|
+
children: (
|
|
1751
|
+
<>
|
|
1752
|
+
{children}
|
|
1753
|
+
<div
|
|
1754
|
+
data-slot="gantt-announcer"
|
|
1755
|
+
aria-live="polite"
|
|
1756
|
+
className="sr-only"
|
|
1757
|
+
/>
|
|
1758
|
+
</>
|
|
1759
|
+
),
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
return (
|
|
1763
|
+
<GanttContext.Provider value={instance}>
|
|
1764
|
+
<GanttViewConfigContext.Provider value={stableViewConfig}>
|
|
1765
|
+
{useRender({
|
|
1766
|
+
defaultTagName: "div",
|
|
1767
|
+
render,
|
|
1768
|
+
props: mergeProps<"div">(defaultProps, rest),
|
|
1769
|
+
})}
|
|
1770
|
+
</GanttViewConfigContext.Provider>
|
|
1771
|
+
</GanttContext.Provider>
|
|
1772
|
+
)
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
export {
|
|
1776
|
+
DEFAULT_ROW_ALIGN,
|
|
1777
|
+
DEFAULT_SCHEDULE_MODE,
|
|
1778
|
+
DEFAULT_VIEW_CONFIG,
|
|
1779
|
+
Gantt,
|
|
1780
|
+
GanttContext,
|
|
1781
|
+
GanttViewConfigContext,
|
|
1782
|
+
resolveScheduleMode,
|
|
1783
|
+
resolveTimelineLines,
|
|
1784
|
+
useGantt,
|
|
1785
|
+
useGanttInteractions,
|
|
1786
|
+
useGanttNavigation,
|
|
1787
|
+
useGanttNodeSchedules,
|
|
1788
|
+
useGanttOccurrences,
|
|
1789
|
+
useGanttScale,
|
|
1790
|
+
useGanttSelection,
|
|
1791
|
+
useGanttSelector,
|
|
1792
|
+
useGanttSettings,
|
|
1793
|
+
useGanttSettingsVersion,
|
|
1794
|
+
useGanttState,
|
|
1795
|
+
useGanttViewConfig,
|
|
1796
|
+
}
|
|
1797
|
+
export type {
|
|
1798
|
+
GanttActivationConfig,
|
|
1799
|
+
GanttApi,
|
|
1800
|
+
GanttBaselineProps,
|
|
1801
|
+
GanttCallbacks,
|
|
1802
|
+
GanttClassNames,
|
|
1803
|
+
GanttColumn,
|
|
1804
|
+
GanttColumnContext,
|
|
1805
|
+
GanttDragIndicatorProps,
|
|
1806
|
+
GanttGridLine,
|
|
1807
|
+
GanttInstance,
|
|
1808
|
+
GanttInternals,
|
|
1809
|
+
GanttMetrics,
|
|
1810
|
+
GanttNodeSchedules,
|
|
1811
|
+
GanttProps,
|
|
1812
|
+
GanttRenderEventProps,
|
|
1813
|
+
GanttResolvedLines,
|
|
1814
|
+
GanttRowBaselineProps,
|
|
1815
|
+
GanttScheduleHintProps,
|
|
1816
|
+
GanttSettings,
|
|
1817
|
+
GanttSummaryProps,
|
|
1818
|
+
GanttTimelineLines,
|
|
1819
|
+
GanttTreePanelConfig,
|
|
1820
|
+
GanttViewConfig,
|
|
1821
|
+
UseGanttStateOptions,
|
|
1822
|
+
}
|