@tuturuuu/ui 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/biome.json +1 -1
- package/package.json +73 -71
- package/src/components/ui/accordion.tsx +1 -1
- package/src/components/ui/breadcrumb.tsx +1 -1
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
- package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
- package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
- package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/carousel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
- package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
- package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
- package/src/components/ui/checkbox.tsx +1 -1
- package/src/components/ui/color-picker.tsx +1 -1
- package/src/components/ui/command.tsx +1 -1
- package/src/components/ui/context-menu.tsx +5 -1
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
- package/src/components/ui/custom/combobox.test.tsx +195 -0
- package/src/components/ui/custom/combobox.tsx +273 -156
- package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
- package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
- package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
- package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
- package/src/components/ui/custom/theme-toggle.tsx +1 -1
- package/src/components/ui/custom/workspace-select.tsx +8 -3
- package/src/components/ui/dialog.test.tsx +52 -0
- package/src/components/ui/dialog.tsx +6 -2
- package/src/components/ui/dropdown-menu.tsx +5 -1
- package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
- package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
- package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
- package/src/components/ui/finance/debts/debts-page.tsx +15 -2
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
- package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/utils.ts +3 -1
- package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
- package/src/components/ui/finance/transactions/form-types.ts +1 -0
- package/src/components/ui/finance/transactions/form.tsx +2 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
- package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
- package/src/components/ui/finance/wallets/form.test.tsx +51 -3
- package/src/components/ui/finance/wallets/form.tsx +15 -4
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
- package/src/components/ui/input-otp.tsx +1 -1
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
- package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
- package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
- package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
- package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
- package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
- package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
- package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
- package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
- package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
- package/src/components/ui/navigation-menu.tsx +1 -1
- package/src/components/ui/pagination.tsx +1 -1
- package/src/components/ui/radio-group.tsx +1 -1
- package/src/components/ui/select.tsx +5 -1
- package/src/components/ui/sheet.tsx +1 -1
- package/src/components/ui/sidebar.tsx +1 -1
- package/src/components/ui/storefront/cart-popover.tsx +61 -0
- package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
- package/src/components/ui/storefront/cart-summary.tsx +93 -154
- package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
- package/src/components/ui/storefront/listing-card.tsx +1 -1
- package/src/components/ui/storefront/merch-sections.tsx +70 -0
- package/src/components/ui/storefront/product-detail.tsx +1 -1
- package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
- package/src/components/ui/storefront/storefront-surface.tsx +101 -166
- package/src/components/ui/storefront/types.ts +4 -0
- package/src/components/ui/storefront/utils.ts +6 -0
- package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
- package/src/components/ui/text-editor/background-color-extension.ts +62 -0
- package/src/components/ui/text-editor/color-controls.tsx +284 -0
- package/src/components/ui/text-editor/editor.tsx +69 -14
- package/src/components/ui/text-editor/extensions.ts +8 -2
- package/src/components/ui/text-editor/highlight-extension.ts +22 -0
- package/src/components/ui/text-editor/tool-bar.tsx +9 -16
- package/src/components/ui/toast.tsx +1 -1
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
- package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
- package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
- package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
- package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
- package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
- package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
- package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
- package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
- package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
- package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
- package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
- package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
- package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
- package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
- package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
- package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
- package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
- package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
- package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
- package/src/declarations.d.ts +1 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
- package/src/hooks/use-calendar-sync.tsx +247 -243
- package/src/hooks/use-calendar.tsx +323 -138
- package/src/hooks/use-task-actions.ts +24 -0
- package/src/hooks/use-user-workspace-config.ts +75 -0
- package/src/hooks/use-workspace-currency.ts +8 -3
- package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
type ListWorkspaceTasksOptions,
|
|
11
11
|
listWorkspaceTasks,
|
|
12
12
|
} from '@tuturuuu/internal-api/tasks';
|
|
13
|
+
import {
|
|
14
|
+
TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID,
|
|
15
|
+
TASK_LAST_BOARD_VIEW_CONFIG_ID,
|
|
16
|
+
} from '@tuturuuu/internal-api/users';
|
|
13
17
|
import type {
|
|
14
18
|
Workspace,
|
|
15
19
|
WorkspaceProductTier,
|
|
@@ -19,6 +23,7 @@ import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
|
19
23
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
20
24
|
import {
|
|
21
25
|
getPersonalExternalStagingListId,
|
|
26
|
+
priorityCompare,
|
|
22
27
|
type WorkspaceLabel,
|
|
23
28
|
} from '@tuturuuu/utils/task-helper';
|
|
24
29
|
import { useTranslations } from 'next-intl';
|
|
@@ -30,25 +35,51 @@ import {
|
|
|
30
35
|
useMemo,
|
|
31
36
|
useState,
|
|
32
37
|
} from 'react';
|
|
38
|
+
import {
|
|
39
|
+
useUpdateUserWorkspaceConfig,
|
|
40
|
+
useUserWorkspaceConfig,
|
|
41
|
+
} from '../../../../hooks/use-user-workspace-config';
|
|
33
42
|
import { KanbanBoard } from '../boards/boardId/kanban';
|
|
43
|
+
import type {
|
|
44
|
+
KanbanDeadlineCollapsedState,
|
|
45
|
+
KanbanDeadlineSection,
|
|
46
|
+
} from '../boards/boardId/kanban/rendering/kanban-deadline-panels';
|
|
34
47
|
import type { TaskFilters } from '../boards/boardId/task-filter';
|
|
35
48
|
import { TimelineBoard } from '../boards/boardId/timeline-board';
|
|
49
|
+
import { DraftsPage } from '../drafts/drafts-page';
|
|
36
50
|
import { useTaskDialog } from '../hooks/useTaskDialog';
|
|
51
|
+
import MyTasksContent from '../my-tasks/my-tasks-content';
|
|
37
52
|
import { BoardHeader, type ListStatusFilter } from '../shared/board-header';
|
|
38
53
|
import { ListView } from '../shared/list-view';
|
|
39
|
-
import {
|
|
54
|
+
import { RecycleBinContent } from '../shared/recycle-bin-panel';
|
|
40
55
|
import { loadBoardConfig } from './board-config-storage';
|
|
56
|
+
import {
|
|
57
|
+
parseSpecialTaskListPins,
|
|
58
|
+
type SpecialTaskListPin,
|
|
59
|
+
serializeSpecialTaskListPins,
|
|
60
|
+
} from './special-task-list-pins';
|
|
41
61
|
|
|
42
|
-
export type ViewType =
|
|
62
|
+
export type ViewType =
|
|
63
|
+
| 'kanban'
|
|
64
|
+
| 'list'
|
|
65
|
+
| 'my_tasks'
|
|
66
|
+
| 'timeline'
|
|
67
|
+
| 'drafts'
|
|
68
|
+
| 'recycle_bin';
|
|
43
69
|
|
|
44
70
|
const HOTKEY_CREATE_TASK = 'C';
|
|
45
71
|
const HOTKEY_GO_TO_KANBAN: ['G', 'K'] = ['G', 'K'];
|
|
46
72
|
const HOTKEY_GO_TO_LIST: ['G', 'L'] = ['G', 'L'];
|
|
73
|
+
const HOTKEY_GO_TO_MY_TASKS: ['G', 'M'] = ['G', 'M'];
|
|
47
74
|
const HOTKEY_GO_TO_TIMELINE: ['G', 'T'] = ['G', 'T'];
|
|
75
|
+
const HOTKEY_GO_TO_DRAFTS: ['G', 'D'] = ['G', 'D'];
|
|
76
|
+
const HOTKEY_GO_TO_RECYCLE_BIN: ['G', 'R'] = ['G', 'R'];
|
|
48
77
|
const EXTERNAL_TASKS_COLLAPSED_STORAGE_PREFIX =
|
|
49
78
|
'personal-board-external-tasks-collapsed';
|
|
50
79
|
const CLOSED_TASK_LIST_COLLAPSED_STORAGE_PREFIX =
|
|
51
80
|
'task-board-closed-list-collapsed';
|
|
81
|
+
const DEADLINE_SECTION_COLLAPSED_STORAGE_PREFIX =
|
|
82
|
+
'task-board-deadline-section-collapsed';
|
|
52
83
|
const DEFAULT_TASK_FILTERS: TaskFilters = {
|
|
53
84
|
labels: [],
|
|
54
85
|
assignees: [],
|
|
@@ -78,6 +109,140 @@ function getClosedTaskListCollapsedStorageKey(boardId: string, listId: string) {
|
|
|
78
109
|
return `${CLOSED_TASK_LIST_COLLAPSED_STORAGE_PREFIX}:${boardId}:${listId}`;
|
|
79
110
|
}
|
|
80
111
|
|
|
112
|
+
function getDeadlineSectionCollapsedStorageKey(
|
|
113
|
+
boardId: string,
|
|
114
|
+
section: KanbanDeadlineSection
|
|
115
|
+
) {
|
|
116
|
+
return `${DEADLINE_SECTION_COLLAPSED_STORAGE_PREFIX}:${boardId}:${section}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function taskMatchesLocalFilters(
|
|
120
|
+
task: Task,
|
|
121
|
+
filters: TaskFilters,
|
|
122
|
+
currentUserId?: string
|
|
123
|
+
) {
|
|
124
|
+
const query = filters.searchQuery?.trim().toLowerCase();
|
|
125
|
+
if (query) {
|
|
126
|
+
const searchableText = [
|
|
127
|
+
task.name,
|
|
128
|
+
task.display_number ? String(task.display_number) : null,
|
|
129
|
+
...(task.labels ?? []).map((label) => label.name),
|
|
130
|
+
...(task.projects ?? []).map((project) => project.name),
|
|
131
|
+
...(task.assignees ?? []).map(
|
|
132
|
+
(assignee) => assignee.display_name ?? assignee.email ?? assignee.handle
|
|
133
|
+
),
|
|
134
|
+
]
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.join(' ')
|
|
137
|
+
.toLowerCase();
|
|
138
|
+
|
|
139
|
+
if (!searchableText.includes(query)) return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
filters.labels.length > 0 &&
|
|
144
|
+
!filters.labels.every((label) =>
|
|
145
|
+
task.labels?.some((taskLabel) => taskLabel.id === label.id)
|
|
146
|
+
)
|
|
147
|
+
) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
filters.projects.length > 0 &&
|
|
153
|
+
!filters.projects.every((project) =>
|
|
154
|
+
task.projects?.some((taskProject) => taskProject.id === project.id)
|
|
155
|
+
)
|
|
156
|
+
) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (
|
|
161
|
+
filters.priorities.length > 0 &&
|
|
162
|
+
(!task.priority || !filters.priorities.includes(task.priority))
|
|
163
|
+
) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (filters.assignees.length > 0) {
|
|
168
|
+
const assigneeIds = new Set(
|
|
169
|
+
filters.assignees.map((assignee) => assignee.id)
|
|
170
|
+
);
|
|
171
|
+
if (!task.assignees?.some((assignee) => assigneeIds.has(assignee.id))) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
filters.includeMyTasks &&
|
|
178
|
+
currentUserId &&
|
|
179
|
+
!task.assignees?.some((assignee) => assignee.id === currentUserId)
|
|
180
|
+
) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (filters.includeUnassigned && (task.assignees?.length ?? 0) > 0) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (filters.dueDateRange?.from || filters.dueDateRange?.to) {
|
|
189
|
+
if (!task.end_date) return false;
|
|
190
|
+
const dueTime = new Date(task.end_date).getTime();
|
|
191
|
+
const fromTime = filters.dueDateRange.from?.getTime() ?? -Infinity;
|
|
192
|
+
const toTime = filters.dueDateRange.to?.getTime() ?? Infinity;
|
|
193
|
+
if (dueTime < fromTime || dueTime > toTime) return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (
|
|
197
|
+
typeof filters.estimationRange?.min === 'number' ||
|
|
198
|
+
typeof filters.estimationRange?.max === 'number'
|
|
199
|
+
) {
|
|
200
|
+
const estimate = task.estimation_points ?? 0;
|
|
201
|
+
const min = filters.estimationRange.min ?? -Infinity;
|
|
202
|
+
const max = filters.estimationRange.max ?? Infinity;
|
|
203
|
+
if (estimate < min || estimate > max) return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getTaskTimestamp(value: string | null | undefined) {
|
|
210
|
+
if (!value) return Number.MAX_SAFE_INTEGER;
|
|
211
|
+
const time = new Date(value).getTime();
|
|
212
|
+
return Number.isFinite(time) ? time : Number.MAX_SAFE_INTEGER;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function sortLocalTasks(tasks: Task[], sortBy: TaskFilters['sortBy']) {
|
|
216
|
+
if (!sortBy) return tasks;
|
|
217
|
+
|
|
218
|
+
return [...tasks].sort((a, b) => {
|
|
219
|
+
switch (sortBy) {
|
|
220
|
+
case 'name-asc':
|
|
221
|
+
return a.name.localeCompare(b.name);
|
|
222
|
+
case 'name-desc':
|
|
223
|
+
return b.name.localeCompare(a.name);
|
|
224
|
+
case 'priority-high':
|
|
225
|
+
return priorityCompare(a.priority ?? null, b.priority ?? null);
|
|
226
|
+
case 'priority-low':
|
|
227
|
+
return priorityCompare(b.priority ?? null, a.priority ?? null);
|
|
228
|
+
case 'due-date-asc':
|
|
229
|
+
return getTaskTimestamp(a.end_date) - getTaskTimestamp(b.end_date);
|
|
230
|
+
case 'due-date-desc':
|
|
231
|
+
return getTaskTimestamp(b.end_date) - getTaskTimestamp(a.end_date);
|
|
232
|
+
case 'created-date-asc':
|
|
233
|
+
return getTaskTimestamp(a.created_at) - getTaskTimestamp(b.created_at);
|
|
234
|
+
case 'created-date-desc':
|
|
235
|
+
return getTaskTimestamp(b.created_at) - getTaskTimestamp(a.created_at);
|
|
236
|
+
case 'estimation-high':
|
|
237
|
+
return (b.estimation_points ?? 0) - (a.estimation_points ?? 0);
|
|
238
|
+
case 'estimation-low':
|
|
239
|
+
return (a.estimation_points ?? 0) - (b.estimation_points ?? 0);
|
|
240
|
+
default:
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
81
246
|
interface Props {
|
|
82
247
|
workspace: Workspace;
|
|
83
248
|
workspaceTier?: WorkspaceProductTier | null;
|
|
@@ -85,9 +250,13 @@ interface Props {
|
|
|
85
250
|
tasks: Task[];
|
|
86
251
|
lists: TaskList[];
|
|
87
252
|
workspaceLabels: WorkspaceLabel[];
|
|
253
|
+
availableViews?: ViewType[];
|
|
88
254
|
canManageBoard?: boolean;
|
|
89
255
|
currentUserId?: string;
|
|
90
256
|
idleBottomIsland?: ReactNode;
|
|
257
|
+
publicHeaderPrefix?: ReactNode;
|
|
258
|
+
publicView?: boolean;
|
|
259
|
+
readOnly?: boolean;
|
|
91
260
|
}
|
|
92
261
|
|
|
93
262
|
export function BoardViews({
|
|
@@ -96,9 +265,13 @@ export function BoardViews({
|
|
|
96
265
|
board,
|
|
97
266
|
tasks,
|
|
98
267
|
lists,
|
|
268
|
+
availableViews,
|
|
99
269
|
canManageBoard = true,
|
|
100
270
|
currentUserId,
|
|
101
271
|
idleBottomIsland,
|
|
272
|
+
publicHeaderPrefix,
|
|
273
|
+
publicView = false,
|
|
274
|
+
readOnly = false,
|
|
102
275
|
}: Props) {
|
|
103
276
|
const t = useTranslations('common');
|
|
104
277
|
const tTasks = useTranslations('ws-tasks');
|
|
@@ -110,6 +283,8 @@ export function BoardViews({
|
|
|
110
283
|
const [closedTaskListsCollapsed, setClosedTaskListsCollapsed] = useState<
|
|
111
284
|
Record<string, boolean>
|
|
112
285
|
>({});
|
|
286
|
+
const [deadlineSectionsCollapsed, setDeadlineSectionsCollapsed] =
|
|
287
|
+
useState<KanbanDeadlineCollapsedState>({});
|
|
113
288
|
const [filters, setFilters] = useState<TaskFilters>(DEFAULT_TASK_FILTERS);
|
|
114
289
|
const [listStatusFilter, setListStatusFilter] =
|
|
115
290
|
useState<ListStatusFilter>('all');
|
|
@@ -117,11 +292,58 @@ export function BoardViews({
|
|
|
117
292
|
const [taskOverrides, setTaskOverrides] = useState<
|
|
118
293
|
Record<string, Partial<Task>>
|
|
119
294
|
>({});
|
|
120
|
-
const [recycleBinOpen, setRecycleBinOpen] = useState(false);
|
|
121
295
|
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false);
|
|
122
296
|
const [kanbanBulkSelectionActive, setKanbanBulkSelectionActive] =
|
|
123
297
|
useState(false);
|
|
124
298
|
const { createTask } = useTaskDialog();
|
|
299
|
+
const localTaskState = readOnly || publicView;
|
|
300
|
+
const { data: pinnedSpecialListsRaw } = useUserWorkspaceConfig(
|
|
301
|
+
effectiveWorkspaceId,
|
|
302
|
+
TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID,
|
|
303
|
+
null,
|
|
304
|
+
{ enabled: !localTaskState }
|
|
305
|
+
);
|
|
306
|
+
const updateUserWorkspaceConfig = useUpdateUserWorkspaceConfig();
|
|
307
|
+
const specialTaskListPins = useMemo(
|
|
308
|
+
() => parseSpecialTaskListPins(pinnedSpecialListsRaw),
|
|
309
|
+
[pinnedSpecialListsRaw]
|
|
310
|
+
);
|
|
311
|
+
const handleSpecialTaskListPinnedChange = useCallback(
|
|
312
|
+
(pin: SpecialTaskListPin, pinned: boolean) => {
|
|
313
|
+
const nextPins = {
|
|
314
|
+
...specialTaskListPins,
|
|
315
|
+
[pin]: pinned,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
if (!pinned) delete nextPins[pin];
|
|
319
|
+
|
|
320
|
+
updateUserWorkspaceConfig.mutate({
|
|
321
|
+
configId: TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID,
|
|
322
|
+
value: serializeSpecialTaskListPins(nextPins),
|
|
323
|
+
workspaceId: effectiveWorkspaceId,
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
[effectiveWorkspaceId, specialTaskListPins, updateUserWorkspaceConfig]
|
|
327
|
+
);
|
|
328
|
+
const enabledViews = useMemo(
|
|
329
|
+
() =>
|
|
330
|
+
availableViews ??
|
|
331
|
+
(publicView || readOnly
|
|
332
|
+
? (['kanban', 'list', 'timeline'] as ViewType[])
|
|
333
|
+
: ([
|
|
334
|
+
'kanban',
|
|
335
|
+
'list',
|
|
336
|
+
'my_tasks',
|
|
337
|
+
'timeline',
|
|
338
|
+
'drafts',
|
|
339
|
+
'recycle_bin',
|
|
340
|
+
] as ViewType[])),
|
|
341
|
+
[availableViews, publicView, readOnly]
|
|
342
|
+
);
|
|
343
|
+
const viewIsEnabled = useCallback(
|
|
344
|
+
(view: ViewType) => !enabledViews || enabledViews.includes(view),
|
|
345
|
+
[enabledViews]
|
|
346
|
+
);
|
|
125
347
|
const sourceScope = filters.sourceScope ?? 'all_visible';
|
|
126
348
|
const sourceWorkspaceIds = filters.sourceWorkspaceIds ?? [];
|
|
127
349
|
const sourceBoardIds = filters.sourceBoardIds ?? [];
|
|
@@ -206,11 +428,21 @@ export function BoardViews({
|
|
|
206
428
|
taskQueryOptions,
|
|
207
429
|
]
|
|
208
430
|
);
|
|
431
|
+
const deadlineTaskQueryOptions = useMemo<ListWorkspaceTasksOptions>(() => {
|
|
432
|
+
const { sortBy: _sortBy, ...filterOptions } = taskQueryOptions;
|
|
433
|
+
return {
|
|
434
|
+
...filterOptions,
|
|
435
|
+
listStatuses: listStatusesForQuery,
|
|
436
|
+
};
|
|
437
|
+
}, [listStatusesForQuery, taskQueryOptions]);
|
|
209
438
|
const viewHotkeyLabels = useMemo(
|
|
210
439
|
() => ({
|
|
211
440
|
kanban: formatHotkeySequence(HOTKEY_GO_TO_KANBAN),
|
|
212
441
|
list: formatHotkeySequence(HOTKEY_GO_TO_LIST),
|
|
442
|
+
my_tasks: formatHotkeySequence(HOTKEY_GO_TO_MY_TASKS),
|
|
213
443
|
timeline: formatHotkeySequence(HOTKEY_GO_TO_TIMELINE),
|
|
444
|
+
drafts: formatHotkeySequence(HOTKEY_GO_TO_DRAFTS),
|
|
445
|
+
recycle_bin: formatHotkeySequence(HOTKEY_GO_TO_RECYCLE_BIN),
|
|
214
446
|
}),
|
|
215
447
|
[]
|
|
216
448
|
);
|
|
@@ -228,6 +460,7 @@ export function BoardViews({
|
|
|
228
460
|
|
|
229
461
|
const primeFullTaskCache = useCallback(
|
|
230
462
|
(nextView: ViewType) => {
|
|
463
|
+
if (localTaskState) return;
|
|
231
464
|
if (nextView !== 'list' && nextView !== 'timeline') return;
|
|
232
465
|
|
|
233
466
|
void queryClient.prefetchQuery({
|
|
@@ -236,20 +469,52 @@ export function BoardViews({
|
|
|
236
469
|
staleTime: 0,
|
|
237
470
|
});
|
|
238
471
|
},
|
|
239
|
-
[board.id, fetchBoardTasks, queryClient, taskFilterKey]
|
|
472
|
+
[board.id, fetchBoardTasks, localTaskState, queryClient, taskFilterKey]
|
|
240
473
|
);
|
|
241
474
|
|
|
242
475
|
const handleViewChange = useCallback(
|
|
243
476
|
(nextView: ViewType) => {
|
|
477
|
+
if (!viewIsEnabled(nextView)) return;
|
|
244
478
|
setCurrentView(nextView);
|
|
245
479
|
primeFullTaskCache(nextView);
|
|
480
|
+
if (!localTaskState) {
|
|
481
|
+
updateUserWorkspaceConfig.mutate({
|
|
482
|
+
configId: TASK_LAST_BOARD_VIEW_CONFIG_ID,
|
|
483
|
+
value: nextView,
|
|
484
|
+
workspaceId: effectiveWorkspaceId,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (typeof window !== 'undefined') {
|
|
489
|
+
const currentUrl = new URL(window.location.href);
|
|
490
|
+
if (!currentUrl.pathname.includes('/tasks/boards/')) return;
|
|
491
|
+
|
|
492
|
+
const params = currentUrl.searchParams;
|
|
493
|
+
if (nextView === 'kanban') {
|
|
494
|
+
params.delete('view');
|
|
495
|
+
} else {
|
|
496
|
+
params.set('view', nextView);
|
|
497
|
+
}
|
|
498
|
+
const nextQuery = params.toString();
|
|
499
|
+
window.history.replaceState(
|
|
500
|
+
window.history.state,
|
|
501
|
+
'',
|
|
502
|
+
`${currentUrl.pathname}${nextQuery ? `?${nextQuery}` : ''}${currentUrl.hash}`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
246
505
|
},
|
|
247
|
-
[
|
|
506
|
+
[
|
|
507
|
+
effectiveWorkspaceId,
|
|
508
|
+
localTaskState,
|
|
509
|
+
primeFullTaskCache,
|
|
510
|
+
updateUserWorkspaceConfig,
|
|
511
|
+
viewIsEnabled,
|
|
512
|
+
]
|
|
248
513
|
);
|
|
249
514
|
|
|
250
515
|
const { data: fullTasks = [], isFetching: isFullTasksFetching } = useQuery({
|
|
251
516
|
queryKey: ['tasks-full', board.id, taskFilterKey],
|
|
252
|
-
enabled: shouldEagerLoadTasks,
|
|
517
|
+
enabled: !localTaskState && shouldEagerLoadTasks,
|
|
253
518
|
queryFn: fetchBoardTasks,
|
|
254
519
|
refetchOnMount: 'always',
|
|
255
520
|
staleTime: 0,
|
|
@@ -273,21 +538,36 @@ export function BoardViews({
|
|
|
273
538
|
|
|
274
539
|
useLayoutEffect(() => {
|
|
275
540
|
const savedConfig = loadBoardConfig(board.id);
|
|
541
|
+
const requestedView =
|
|
542
|
+
typeof window === 'undefined' ||
|
|
543
|
+
!window.location.pathname.includes('/tasks/boards/')
|
|
544
|
+
? null
|
|
545
|
+
: (new URLSearchParams(window.location.search).get(
|
|
546
|
+
'view'
|
|
547
|
+
) as ViewType | null);
|
|
548
|
+
const defaultView = enabledViews?.[0] ?? 'kanban';
|
|
549
|
+
const initialView =
|
|
550
|
+
requestedView && viewIsEnabled(requestedView) ? requestedView : null;
|
|
276
551
|
|
|
277
552
|
if (!savedConfig) {
|
|
278
|
-
setCurrentView(
|
|
553
|
+
setCurrentView(initialView ?? defaultView);
|
|
279
554
|
setFilters(DEFAULT_TASK_FILTERS);
|
|
280
555
|
setListStatusFilter('all');
|
|
281
556
|
return;
|
|
282
557
|
}
|
|
283
558
|
|
|
284
|
-
setCurrentView(
|
|
559
|
+
setCurrentView(
|
|
560
|
+
initialView ??
|
|
561
|
+
(viewIsEnabled(savedConfig.currentView)
|
|
562
|
+
? savedConfig.currentView
|
|
563
|
+
: defaultView)
|
|
564
|
+
);
|
|
285
565
|
setFilters({
|
|
286
566
|
...DEFAULT_TASK_FILTERS,
|
|
287
567
|
...savedConfig.filters,
|
|
288
568
|
});
|
|
289
569
|
setListStatusFilter(savedConfig.listStatusFilter);
|
|
290
|
-
}, [board.id]);
|
|
570
|
+
}, [board.id, enabledViews, viewIsEnabled]);
|
|
291
571
|
|
|
292
572
|
useEffect(() => {
|
|
293
573
|
if (!workspace.personal || typeof window === 'undefined') {
|
|
@@ -308,6 +588,8 @@ export function BoardViews({
|
|
|
308
588
|
|
|
309
589
|
const handleExternalTasksCollapsedChange = useCallback(
|
|
310
590
|
(collapsed: boolean) => {
|
|
591
|
+
if (collapsed && specialTaskListPins.external_tasks) return;
|
|
592
|
+
|
|
311
593
|
setExternalTasksCollapsed(collapsed);
|
|
312
594
|
|
|
313
595
|
if (!workspace.personal || typeof window === 'undefined') return;
|
|
@@ -317,7 +599,7 @@ export function BoardViews({
|
|
|
317
599
|
String(collapsed)
|
|
318
600
|
);
|
|
319
601
|
},
|
|
320
|
-
[board.id, workspace.personal]
|
|
602
|
+
[board.id, specialTaskListPins.external_tasks, workspace.personal]
|
|
321
603
|
);
|
|
322
604
|
|
|
323
605
|
useEffect(() => {
|
|
@@ -353,6 +635,16 @@ export function BoardViews({
|
|
|
353
635
|
|
|
354
636
|
const handleTaskListCollapsedChange = useCallback(
|
|
355
637
|
(listId: string, collapsed: boolean) => {
|
|
638
|
+
if (
|
|
639
|
+
collapsed &&
|
|
640
|
+
specialTaskListPins.closed_tasks &&
|
|
641
|
+
boardLists.some(
|
|
642
|
+
(list) => list.id === listId && list.status === 'closed'
|
|
643
|
+
)
|
|
644
|
+
) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
356
648
|
setClosedTaskListsCollapsed((previous) => ({
|
|
357
649
|
...previous,
|
|
358
650
|
[listId]: collapsed,
|
|
@@ -365,7 +657,54 @@ export function BoardViews({
|
|
|
365
657
|
String(collapsed)
|
|
366
658
|
);
|
|
367
659
|
},
|
|
368
|
-
[board.id]
|
|
660
|
+
[board.id, boardLists, specialTaskListPins.closed_tasks]
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
useEffect(() => {
|
|
664
|
+
setDeadlineSectionsCollapsed((previous) => {
|
|
665
|
+
const next: KanbanDeadlineCollapsedState = {};
|
|
666
|
+
|
|
667
|
+
for (const section of ['overdue', 'upcoming'] as const) {
|
|
668
|
+
const storedValue =
|
|
669
|
+
typeof window === 'undefined'
|
|
670
|
+
? null
|
|
671
|
+
: window.localStorage.getItem(
|
|
672
|
+
getDeadlineSectionCollapsedStorageKey(board.id, section)
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
next[section] =
|
|
676
|
+
storedValue === null
|
|
677
|
+
? (previous[section] ?? false)
|
|
678
|
+
: storedValue === 'true';
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return next;
|
|
682
|
+
});
|
|
683
|
+
}, [board.id]);
|
|
684
|
+
|
|
685
|
+
const handleDeadlineSectionCollapsedChange = useCallback(
|
|
686
|
+
(section: KanbanDeadlineSection, collapsed: boolean) => {
|
|
687
|
+
if (
|
|
688
|
+
collapsed &&
|
|
689
|
+
((section === 'overdue' && specialTaskListPins.overdue) ||
|
|
690
|
+
(section === 'upcoming' && specialTaskListPins.upcoming))
|
|
691
|
+
) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
setDeadlineSectionsCollapsed((previous) => ({
|
|
696
|
+
...previous,
|
|
697
|
+
[section]: collapsed,
|
|
698
|
+
}));
|
|
699
|
+
|
|
700
|
+
if (typeof window === 'undefined') return;
|
|
701
|
+
|
|
702
|
+
window.localStorage.setItem(
|
|
703
|
+
getDeadlineSectionCollapsedStorageKey(board.id, section),
|
|
704
|
+
String(collapsed)
|
|
705
|
+
);
|
|
706
|
+
},
|
|
707
|
+
[board.id, specialTaskListPins.overdue, specialTaskListPins.upcoming]
|
|
369
708
|
);
|
|
370
709
|
|
|
371
710
|
const externalStagingList = useMemo<TaskList | null>(() => {
|
|
@@ -383,12 +722,15 @@ export function BoardViews({
|
|
|
383
722
|
color: 'CYAN',
|
|
384
723
|
position: Number.MIN_SAFE_INTEGER,
|
|
385
724
|
is_external_staging: true,
|
|
386
|
-
is_external_collapsed:
|
|
725
|
+
is_external_collapsed: specialTaskListPins.external_tasks
|
|
726
|
+
? false
|
|
727
|
+
: externalTasksCollapsed,
|
|
387
728
|
};
|
|
388
729
|
}, [
|
|
389
730
|
board.created_at,
|
|
390
731
|
board.id,
|
|
391
732
|
externalTasksCollapsed,
|
|
733
|
+
specialTaskListPins.external_tasks,
|
|
392
734
|
tTasks,
|
|
393
735
|
workspace.personal,
|
|
394
736
|
]);
|
|
@@ -400,19 +742,44 @@ export function BoardViews({
|
|
|
400
742
|
list.status === 'closed'
|
|
401
743
|
? {
|
|
402
744
|
...list,
|
|
403
|
-
is_collapsed:
|
|
745
|
+
is_collapsed: specialTaskListPins.closed_tasks
|
|
746
|
+
? false
|
|
747
|
+
: (closedTaskListsCollapsed[list.id] ?? true),
|
|
404
748
|
}
|
|
405
749
|
: list
|
|
406
750
|
);
|
|
407
751
|
return externalStagingList
|
|
408
752
|
? [externalStagingList, ...realLists]
|
|
409
753
|
: realLists;
|
|
410
|
-
}, [
|
|
754
|
+
}, [
|
|
755
|
+
boardLists,
|
|
756
|
+
closedTaskListsCollapsed,
|
|
757
|
+
externalStagingList,
|
|
758
|
+
specialTaskListPins.closed_tasks,
|
|
759
|
+
]);
|
|
760
|
+
|
|
761
|
+
const effectiveDeadlineSectionsCollapsed =
|
|
762
|
+
useMemo<KanbanDeadlineCollapsedState>(
|
|
763
|
+
() => ({
|
|
764
|
+
overdue: specialTaskListPins.overdue
|
|
765
|
+
? false
|
|
766
|
+
: deadlineSectionsCollapsed.overdue,
|
|
767
|
+
upcoming: specialTaskListPins.upcoming
|
|
768
|
+
? false
|
|
769
|
+
: deadlineSectionsCollapsed.upcoming,
|
|
770
|
+
}),
|
|
771
|
+
[
|
|
772
|
+
deadlineSectionsCollapsed.overdue,
|
|
773
|
+
deadlineSectionsCollapsed.upcoming,
|
|
774
|
+
specialTaskListPins.overdue,
|
|
775
|
+
specialTaskListPins.upcoming,
|
|
776
|
+
]
|
|
777
|
+
);
|
|
411
778
|
|
|
412
779
|
const { data: filteredListCounts, isFetching: isFilteredListCountsFetching } =
|
|
413
780
|
useQuery({
|
|
414
781
|
queryKey: ['task-list-counts', board.id, taskFilterKey],
|
|
415
|
-
enabled: hasTaskFilters,
|
|
782
|
+
enabled: !localTaskState && hasTaskFilters,
|
|
416
783
|
queryFn: async () => {
|
|
417
784
|
const result = await listWorkspaceTasks(effectiveWorkspaceId, {
|
|
418
785
|
...taskQueryOptions,
|
|
@@ -427,6 +794,31 @@ export function BoardViews({
|
|
|
427
794
|
staleTime: 30_000,
|
|
428
795
|
});
|
|
429
796
|
|
|
797
|
+
const locallyFilteredTasks = useMemo(
|
|
798
|
+
() =>
|
|
799
|
+
sortLocalTasks(
|
|
800
|
+
tasks.filter((task) =>
|
|
801
|
+
taskMatchesLocalFilters(task, filters, currentUserId)
|
|
802
|
+
),
|
|
803
|
+
filters.sortBy
|
|
804
|
+
),
|
|
805
|
+
[currentUserId, filters, tasks]
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
const localListCounts = useMemo(() => {
|
|
809
|
+
if (!localTaskState || !hasTaskFilters) return null;
|
|
810
|
+
|
|
811
|
+
const counts = new Map<string, number>();
|
|
812
|
+
for (const task of locallyFilteredTasks) {
|
|
813
|
+
counts.set(task.list_id, (counts.get(task.list_id) ?? 0) + 1);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return [...counts.entries()].map(([list_id, count]) => ({
|
|
817
|
+
list_id,
|
|
818
|
+
count,
|
|
819
|
+
}));
|
|
820
|
+
}, [hasTaskFilters, localTaskState, locallyFilteredTasks]);
|
|
821
|
+
|
|
430
822
|
// Filter lists based on selected status filter
|
|
431
823
|
const statusFilteredLists = useMemo(() => {
|
|
432
824
|
if (listStatusFilter === 'all') return activeLists;
|
|
@@ -439,18 +831,27 @@ export function BoardViews({
|
|
|
439
831
|
}, [activeLists, listStatusFilter]);
|
|
440
832
|
|
|
441
833
|
const filteredLists = useMemo(() => {
|
|
442
|
-
|
|
834
|
+
const listCounts = localTaskState ? localListCounts : filteredListCounts;
|
|
835
|
+
if (!hasTaskFilters || !listCounts) return statusFilteredLists;
|
|
443
836
|
|
|
444
837
|
const countByListId = new Map(
|
|
445
|
-
|
|
838
|
+
listCounts.map((entry) => [entry.list_id, entry.count] as const)
|
|
446
839
|
);
|
|
447
840
|
|
|
448
841
|
return statusFilteredLists.filter(
|
|
449
842
|
(list) => (countByListId.get(list.id) ?? 0) > 0
|
|
450
843
|
);
|
|
451
|
-
}, [
|
|
844
|
+
}, [
|
|
845
|
+
filteredListCounts,
|
|
846
|
+
hasTaskFilters,
|
|
847
|
+
localListCounts,
|
|
848
|
+
localTaskState,
|
|
849
|
+
statusFilteredLists,
|
|
850
|
+
]);
|
|
452
851
|
|
|
453
852
|
const sourceTasks = useMemo(() => {
|
|
853
|
+
if (localTaskState) return locallyFilteredTasks;
|
|
854
|
+
|
|
454
855
|
if (!shouldEagerLoadTasks) return tasks;
|
|
455
856
|
|
|
456
857
|
if (fullTasks.length === 0) {
|
|
@@ -475,7 +876,15 @@ export function BoardViews({
|
|
|
475
876
|
}
|
|
476
877
|
|
|
477
878
|
return merged;
|
|
478
|
-
}, [
|
|
879
|
+
}, [
|
|
880
|
+
fullTasks,
|
|
881
|
+
hasServerTaskQuery,
|
|
882
|
+
localTaskState,
|
|
883
|
+
locallyFilteredTasks,
|
|
884
|
+
shouldEagerLoadTasks,
|
|
885
|
+
sourceScope,
|
|
886
|
+
tasks,
|
|
887
|
+
]);
|
|
479
888
|
|
|
480
889
|
// Keep only tasks that belong to the server-visible lists/status scope.
|
|
481
890
|
const filteredTasks = useMemo(() => {
|
|
@@ -531,7 +940,11 @@ export function BoardViews({
|
|
|
531
940
|
createTask(board.id, targetList.id, selectableLists, filters);
|
|
532
941
|
},
|
|
533
942
|
{
|
|
534
|
-
enabled:
|
|
943
|
+
enabled:
|
|
944
|
+
!readOnly &&
|
|
945
|
+
currentView !== 'drafts' &&
|
|
946
|
+
currentView !== 'recycle_bin' &&
|
|
947
|
+
filteredLists.some((list) => !list.is_external_staging),
|
|
535
948
|
ignoreInputs: true,
|
|
536
949
|
preventDefault: true,
|
|
537
950
|
}
|
|
@@ -543,6 +956,7 @@ export function BoardViews({
|
|
|
543
956
|
handleViewChange('kanban');
|
|
544
957
|
},
|
|
545
958
|
{
|
|
959
|
+
enabled: viewIsEnabled('kanban'),
|
|
546
960
|
ignoreInputs: true,
|
|
547
961
|
preventDefault: true,
|
|
548
962
|
}
|
|
@@ -554,6 +968,19 @@ export function BoardViews({
|
|
|
554
968
|
handleViewChange('list');
|
|
555
969
|
},
|
|
556
970
|
{
|
|
971
|
+
enabled: viewIsEnabled('list'),
|
|
972
|
+
ignoreInputs: true,
|
|
973
|
+
preventDefault: true,
|
|
974
|
+
}
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
useHotkeySequence(
|
|
978
|
+
HOTKEY_GO_TO_MY_TASKS,
|
|
979
|
+
() => {
|
|
980
|
+
handleViewChange('my_tasks');
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
enabled: viewIsEnabled('my_tasks'),
|
|
557
984
|
ignoreInputs: true,
|
|
558
985
|
preventDefault: true,
|
|
559
986
|
}
|
|
@@ -565,6 +992,31 @@ export function BoardViews({
|
|
|
565
992
|
handleViewChange('timeline');
|
|
566
993
|
},
|
|
567
994
|
{
|
|
995
|
+
enabled: viewIsEnabled('timeline'),
|
|
996
|
+
ignoreInputs: true,
|
|
997
|
+
preventDefault: true,
|
|
998
|
+
}
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
useHotkeySequence(
|
|
1002
|
+
HOTKEY_GO_TO_DRAFTS,
|
|
1003
|
+
() => {
|
|
1004
|
+
handleViewChange('drafts');
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
enabled: viewIsEnabled('drafts'),
|
|
1008
|
+
ignoreInputs: true,
|
|
1009
|
+
preventDefault: true,
|
|
1010
|
+
}
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
useHotkeySequence(
|
|
1014
|
+
HOTKEY_GO_TO_RECYCLE_BIN,
|
|
1015
|
+
() => {
|
|
1016
|
+
handleViewChange('recycle_bin');
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
enabled: viewIsEnabled('recycle_bin'),
|
|
568
1020
|
ignoreInputs: true,
|
|
569
1021
|
preventDefault: true,
|
|
570
1022
|
}
|
|
@@ -583,15 +1035,45 @@ export function BoardViews({
|
|
|
583
1035
|
lists={filteredLists}
|
|
584
1036
|
isLoading={false}
|
|
585
1037
|
disableSort={!!filters.sortBy}
|
|
1038
|
+
deadlineTaskQueryOptions={deadlineTaskQueryOptions}
|
|
586
1039
|
listStatusFilter={listStatusFilter}
|
|
587
1040
|
filters={filters}
|
|
588
|
-
isMultiSelectMode={isMultiSelectMode}
|
|
589
|
-
setIsMultiSelectMode={setIsMultiSelectMode}
|
|
1041
|
+
isMultiSelectMode={readOnly ? false : isMultiSelectMode}
|
|
1042
|
+
setIsMultiSelectMode={readOnly ? () => {} : setIsMultiSelectMode}
|
|
590
1043
|
onExternalTasksCollapsedChange={handleExternalTasksCollapsedChange}
|
|
591
1044
|
onTaskListCollapsedChange={handleTaskListCollapsedChange}
|
|
1045
|
+
deadlineSectionsCollapsed={effectiveDeadlineSectionsCollapsed}
|
|
1046
|
+
onDeadlineSectionCollapsedChange={
|
|
1047
|
+
handleDeadlineSectionCollapsedChange
|
|
1048
|
+
}
|
|
1049
|
+
specialTaskListPins={specialTaskListPins}
|
|
1050
|
+
onSpecialTaskListPinnedChange={handleSpecialTaskListPinnedChange}
|
|
592
1051
|
onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
|
|
1052
|
+
readOnly={readOnly}
|
|
593
1053
|
/>
|
|
594
1054
|
);
|
|
1055
|
+
case 'my_tasks':
|
|
1056
|
+
return (
|
|
1057
|
+
<div className="h-full overflow-y-auto p-3 sm:p-4">
|
|
1058
|
+
<div className="mx-auto max-w-5xl pb-20">
|
|
1059
|
+
{currentUserId ? (
|
|
1060
|
+
<MyTasksContent
|
|
1061
|
+
disableAutoCreateBoard
|
|
1062
|
+
embedded
|
|
1063
|
+
initialBoard={{
|
|
1064
|
+
id: board.id,
|
|
1065
|
+
name: board.name ?? null,
|
|
1066
|
+
}}
|
|
1067
|
+
initialLists={boardLists}
|
|
1068
|
+
initialListId={board.default_list_id ?? undefined}
|
|
1069
|
+
isPersonal={workspace.personal}
|
|
1070
|
+
userId={currentUserId}
|
|
1071
|
+
wsId={effectiveWorkspaceId}
|
|
1072
|
+
/>
|
|
1073
|
+
) : null}
|
|
1074
|
+
</div>
|
|
1075
|
+
</div>
|
|
1076
|
+
);
|
|
595
1077
|
case 'list':
|
|
596
1078
|
return (
|
|
597
1079
|
<ListView
|
|
@@ -602,6 +1084,7 @@ export function BoardViews({
|
|
|
602
1084
|
isPersonalWorkspace={workspace.personal}
|
|
603
1085
|
preserveTaskOrder={!!filters.sortBy}
|
|
604
1086
|
searchQuery={filters.searchQuery}
|
|
1087
|
+
readOnly={readOnly}
|
|
605
1088
|
/>
|
|
606
1089
|
);
|
|
607
1090
|
case 'timeline':
|
|
@@ -614,6 +1097,66 @@ export function BoardViews({
|
|
|
614
1097
|
onTaskPartialUpdate={handleTaskPartialUpdate}
|
|
615
1098
|
/>
|
|
616
1099
|
);
|
|
1100
|
+
case 'drafts':
|
|
1101
|
+
return (
|
|
1102
|
+
<div className="h-full overflow-y-auto p-3 sm:p-4">
|
|
1103
|
+
<DraftsPage
|
|
1104
|
+
boardId={board.id}
|
|
1105
|
+
includeUnassignedForBoard
|
|
1106
|
+
wsId={effectiveWorkspaceId}
|
|
1107
|
+
/>
|
|
1108
|
+
</div>
|
|
1109
|
+
);
|
|
1110
|
+
case 'recycle_bin':
|
|
1111
|
+
return (
|
|
1112
|
+
<RecycleBinContent
|
|
1113
|
+
active
|
|
1114
|
+
boardId={board.id}
|
|
1115
|
+
className="h-full"
|
|
1116
|
+
lists={boardLists}
|
|
1117
|
+
translations={{
|
|
1118
|
+
recycleBin: t('recycle_bin'),
|
|
1119
|
+
recycleBinDescription: t('recycle_bin_description'),
|
|
1120
|
+
noDeletedTasks: t('no_deleted_tasks'),
|
|
1121
|
+
deletedTasksWillAppearHere: t('deleted_tasks_will_appear_here'),
|
|
1122
|
+
selectedOfTotal: t('selected_of_total', {
|
|
1123
|
+
selected: '{selected}',
|
|
1124
|
+
total: '{total}',
|
|
1125
|
+
}),
|
|
1126
|
+
deletedTasksCount: t('deleted_tasks_count', { count: '{count}' }),
|
|
1127
|
+
restore: t('restore'),
|
|
1128
|
+
delete: t('delete'),
|
|
1129
|
+
restoreTasksTitle: t('restore_tasks_title', { count: '{count}' }),
|
|
1130
|
+
restoreTasksDescription: t('restore_tasks_description'),
|
|
1131
|
+
cancel: t('cancel'),
|
|
1132
|
+
restoring: t('restoring'),
|
|
1133
|
+
permanentlyDeleteTitle: t('permanently_delete_title', {
|
|
1134
|
+
count: '{count}',
|
|
1135
|
+
}),
|
|
1136
|
+
permanentlyDeleteDescription: t('permanently_delete_description'),
|
|
1137
|
+
deleting: t('deleting'),
|
|
1138
|
+
deletePermanently: t('delete_permanently'),
|
|
1139
|
+
noListsAvailable: t('no_lists_available'),
|
|
1140
|
+
restoredTasks: t('restored_tasks', { count: '{count}' }),
|
|
1141
|
+
failedToRestore: t('failed_to_restore'),
|
|
1142
|
+
permanentlyDeleted: t('permanently_deleted', {
|
|
1143
|
+
count: '{count}',
|
|
1144
|
+
}),
|
|
1145
|
+
failedToDelete: t('failed_to_delete'),
|
|
1146
|
+
deletedAgo: t('deleted_ago', { time: '{time}' }),
|
|
1147
|
+
fromList: t('from_list', { list: '{list}' }),
|
|
1148
|
+
nProjects: t('n_projects', { count: '{count}' }),
|
|
1149
|
+
selectAllTasks: t('select_all_tasks'),
|
|
1150
|
+
selectTask: t('select_task', { name: '{name}' }),
|
|
1151
|
+
critical: tBoards('dialog.priority.critical'),
|
|
1152
|
+
high: tBoards('dialog.priority.high'),
|
|
1153
|
+
normal: tBoards('dialog.priority.normal'),
|
|
1154
|
+
low: tBoards('dialog.priority.low'),
|
|
1155
|
+
unknownList: t('unknown_list'),
|
|
1156
|
+
}}
|
|
1157
|
+
wsId={effectiveWorkspaceId}
|
|
1158
|
+
/>
|
|
1159
|
+
);
|
|
617
1160
|
default:
|
|
618
1161
|
return (
|
|
619
1162
|
<KanbanBoard
|
|
@@ -625,19 +1168,28 @@ export function BoardViews({
|
|
|
625
1168
|
lists={filteredLists}
|
|
626
1169
|
isLoading={false}
|
|
627
1170
|
disableSort={!!filters.sortBy}
|
|
1171
|
+
deadlineTaskQueryOptions={deadlineTaskQueryOptions}
|
|
628
1172
|
listStatusFilter={listStatusFilter}
|
|
629
1173
|
filters={filters}
|
|
630
|
-
isMultiSelectMode={isMultiSelectMode}
|
|
631
|
-
setIsMultiSelectMode={setIsMultiSelectMode}
|
|
1174
|
+
isMultiSelectMode={readOnly ? false : isMultiSelectMode}
|
|
1175
|
+
setIsMultiSelectMode={readOnly ? () => {} : setIsMultiSelectMode}
|
|
632
1176
|
onExternalTasksCollapsedChange={handleExternalTasksCollapsedChange}
|
|
633
1177
|
onTaskListCollapsedChange={handleTaskListCollapsedChange}
|
|
1178
|
+
deadlineSectionsCollapsed={effectiveDeadlineSectionsCollapsed}
|
|
1179
|
+
onDeadlineSectionCollapsedChange={
|
|
1180
|
+
handleDeadlineSectionCollapsedChange
|
|
1181
|
+
}
|
|
1182
|
+
specialTaskListPins={specialTaskListPins}
|
|
1183
|
+
onSpecialTaskListPinnedChange={handleSpecialTaskListPinnedChange}
|
|
634
1184
|
onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
|
|
1185
|
+
readOnly={readOnly}
|
|
635
1186
|
/>
|
|
636
1187
|
);
|
|
637
1188
|
}
|
|
638
1189
|
};
|
|
639
1190
|
|
|
640
1191
|
const showIdleBottomIsland =
|
|
1192
|
+
!readOnly &&
|
|
641
1193
|
!!idleBottomIsland &&
|
|
642
1194
|
(currentView !== 'kanban' || !kanbanBulkSelectionActive);
|
|
643
1195
|
|
|
@@ -655,62 +1207,22 @@ export function BoardViews({
|
|
|
655
1207
|
listStatusFilter={listStatusFilter}
|
|
656
1208
|
onListStatusFilterChange={setListStatusFilter}
|
|
657
1209
|
isPersonalWorkspace={workspace.personal}
|
|
658
|
-
isSearching={
|
|
1210
|
+
isSearching={
|
|
1211
|
+
!localTaskState &&
|
|
1212
|
+
(isFullTasksFetching || isFilteredListCountsFetching)
|
|
1213
|
+
}
|
|
659
1214
|
lists={boardLists}
|
|
660
1215
|
onUpdate={handleUpdate}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
hideActions={!canManageBoard}
|
|
1216
|
+
isMultiSelectMode={readOnly ? false : isMultiSelectMode}
|
|
1217
|
+
setIsMultiSelectMode={readOnly ? () => {} : setIsMultiSelectMode}
|
|
1218
|
+
availableViews={enabledViews ?? undefined}
|
|
1219
|
+
hideActions={!canManageBoard || readOnly}
|
|
1220
|
+
publicView={publicView}
|
|
1221
|
+
readOnly={readOnly}
|
|
1222
|
+
titlePrefix={publicHeaderPrefix}
|
|
665
1223
|
/>
|
|
666
1224
|
<div className="h-full overflow-hidden">{renderView()}</div>
|
|
667
1225
|
{showIdleBottomIsland ? idleBottomIsland : null}
|
|
668
|
-
|
|
669
|
-
<RecycleBinPanel
|
|
670
|
-
open={recycleBinOpen}
|
|
671
|
-
onOpenChange={setRecycleBinOpen}
|
|
672
|
-
wsId={effectiveWorkspaceId}
|
|
673
|
-
boardId={board.id}
|
|
674
|
-
lists={boardLists}
|
|
675
|
-
translations={{
|
|
676
|
-
recycleBin: t('recycle_bin'),
|
|
677
|
-
recycleBinDescription: t('recycle_bin_description'),
|
|
678
|
-
noDeletedTasks: t('no_deleted_tasks'),
|
|
679
|
-
deletedTasksWillAppearHere: t('deleted_tasks_will_appear_here'),
|
|
680
|
-
selectedOfTotal: t('selected_of_total', {
|
|
681
|
-
selected: '{selected}',
|
|
682
|
-
total: '{total}',
|
|
683
|
-
}),
|
|
684
|
-
deletedTasksCount: t('deleted_tasks_count', { count: '{count}' }),
|
|
685
|
-
restore: t('restore'),
|
|
686
|
-
delete: t('delete'),
|
|
687
|
-
restoreTasksTitle: t('restore_tasks_title', { count: '{count}' }),
|
|
688
|
-
restoreTasksDescription: t('restore_tasks_description'),
|
|
689
|
-
cancel: t('cancel'),
|
|
690
|
-
restoring: t('restoring'),
|
|
691
|
-
permanentlyDeleteTitle: t('permanently_delete_title', {
|
|
692
|
-
count: '{count}',
|
|
693
|
-
}),
|
|
694
|
-
permanentlyDeleteDescription: t('permanently_delete_description'),
|
|
695
|
-
deleting: t('deleting'),
|
|
696
|
-
deletePermanently: t('delete_permanently'),
|
|
697
|
-
noListsAvailable: t('no_lists_available'),
|
|
698
|
-
restoredTasks: t('restored_tasks', { count: '{count}' }),
|
|
699
|
-
failedToRestore: t('failed_to_restore'),
|
|
700
|
-
permanentlyDeleted: t('permanently_deleted', { count: '{count}' }),
|
|
701
|
-
failedToDelete: t('failed_to_delete'),
|
|
702
|
-
deletedAgo: t('deleted_ago', { time: '{time}' }),
|
|
703
|
-
fromList: t('from_list', { list: '{list}' }),
|
|
704
|
-
nProjects: t('n_projects', { count: '{count}' }),
|
|
705
|
-
selectAllTasks: t('select_all_tasks'),
|
|
706
|
-
selectTask: t('select_task', { name: '{name}' }),
|
|
707
|
-
critical: tBoards('dialog.priority.critical'),
|
|
708
|
-
high: tBoards('dialog.priority.high'),
|
|
709
|
-
normal: tBoards('dialog.priority.normal'),
|
|
710
|
-
low: tBoards('dialog.priority.low'),
|
|
711
|
-
unknownList: t('unknown_list'),
|
|
712
|
-
}}
|
|
713
|
-
/>
|
|
714
1226
|
</div>
|
|
715
1227
|
);
|
|
716
1228
|
}
|