@tuturuuu/ui 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/package.json +6 -5
- package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +27 -1
- package/src/components/ui/custom/nav-link.test.tsx +165 -0
- package/src/components/ui/custom/nav-link.tsx +69 -11
- package/src/components/ui/custom/navigation.tsx +1 -0
- package/src/components/ui/custom/settings/task-settings.tsx +104 -0
- package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
- package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
- package/src/components/ui/custom/settings-dialog-search.ts +75 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +63 -27
- package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
- package/src/components/ui/custom/workspace-select.tsx +17 -16
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +16 -0
- package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +4 -328
- package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +50 -37
- package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +263 -21
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +133 -14
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +112 -54
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +8 -2
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +29 -14
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +24 -1
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +7 -1
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +80 -8
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +15 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
- package/src/components/ui/tu-do/boards/form.tsx +1 -1
- package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
- package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
- package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
- package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
- package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
- package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +116 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +38 -0
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +128 -7
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +222 -9
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +21 -0
- package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
- package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +14 -4
- package/src/components/ui/tu-do/shared/board-header.tsx +8 -1
- package/src/components/ui/tu-do/shared/board-switcher.tsx +70 -38
- package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
- package/src/components/ui/tu-do/shared/board-views.tsx +49 -69
- package/src/components/ui/tu-do/shared/list-view.tsx +21 -3
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
- package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
- package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +9 -0
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
- package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
- package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
- package/src/hooks/useBoardPresence.ts +364 -0
- package/src/hooks/useBoardRealtimeEventHandler.ts +34 -90
- package/src/lib/workspace-actions.ts +2 -6
|
@@ -16,9 +16,14 @@ export function ParentSection({
|
|
|
16
16
|
onRemoveParent,
|
|
17
17
|
onNavigateToTask,
|
|
18
18
|
onAddParentTask,
|
|
19
|
+
searchOpen: controlledSearchOpen,
|
|
20
|
+
onSearchOpenChange,
|
|
19
21
|
disabled,
|
|
20
22
|
}: ParentSectionProps) {
|
|
21
|
-
const [
|
|
23
|
+
const [uncontrolledSearchOpen, setUncontrolledSearchOpen] =
|
|
24
|
+
React.useState(false);
|
|
25
|
+
const searchOpen = controlledSearchOpen ?? uncontrolledSearchOpen;
|
|
26
|
+
const setSearchOpen = onSearchOpenChange ?? setUncontrolledSearchOpen;
|
|
22
27
|
|
|
23
28
|
const excludeIds = React.useMemo(() => {
|
|
24
29
|
const ids = taskId ? [taskId, ...childTaskIds] : childTaskIds;
|
|
@@ -16,9 +16,14 @@ export function RelatedSection({
|
|
|
16
16
|
onRemoveRelated,
|
|
17
17
|
onNavigateToTask,
|
|
18
18
|
onAddRelatedTaskDialog,
|
|
19
|
+
searchOpen: controlledSearchOpen,
|
|
20
|
+
onSearchOpenChange,
|
|
19
21
|
disabled,
|
|
20
22
|
}: RelatedSectionProps) {
|
|
21
|
-
const [
|
|
23
|
+
const [uncontrolledSearchOpen, setUncontrolledSearchOpen] =
|
|
24
|
+
React.useState(false);
|
|
25
|
+
const searchOpen = controlledSearchOpen ?? uncontrolledSearchOpen;
|
|
26
|
+
const setSearchOpen = onSearchOpenChange ?? setUncontrolledSearchOpen;
|
|
22
27
|
|
|
23
28
|
const excludeIds = React.useMemo(() => {
|
|
24
29
|
const ids = taskId ? [taskId] : [];
|
|
@@ -16,9 +16,14 @@ export function SubtasksSection({
|
|
|
16
16
|
onAddSubtask,
|
|
17
17
|
onAddExistingAsSubtask,
|
|
18
18
|
isSaving,
|
|
19
|
+
searchOpen: controlledSearchOpen,
|
|
20
|
+
onSearchOpenChange,
|
|
19
21
|
disabled,
|
|
20
22
|
}: SubtasksSectionProps) {
|
|
21
|
-
const [
|
|
23
|
+
const [uncontrolledSearchOpen, setUncontrolledSearchOpen] =
|
|
24
|
+
React.useState(false);
|
|
25
|
+
const searchOpen = controlledSearchOpen ?? uncontrolledSearchOpen;
|
|
26
|
+
const setSearchOpen = onSearchOpenChange ?? setUncontrolledSearchOpen;
|
|
22
27
|
|
|
23
28
|
const excludeIds = React.useMemo(() => {
|
|
24
29
|
const ids = taskId ? [taskId] : [];
|
|
@@ -122,6 +122,8 @@ export interface ParentSectionProps {
|
|
|
122
122
|
onRemoveParent: () => void;
|
|
123
123
|
onNavigateToTask: (taskId: string) => void;
|
|
124
124
|
onAddParentTask?: () => void; // Opens dialog to create new parent task
|
|
125
|
+
searchOpen?: boolean;
|
|
126
|
+
onSearchOpenChange?: (open: boolean) => void;
|
|
125
127
|
disabled?: boolean;
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -137,6 +139,8 @@ export interface SubtasksSectionProps {
|
|
|
137
139
|
onAddSubtask?: () => void;
|
|
138
140
|
onAddExistingAsSubtask?: (task: RelatedTaskInfo) => Promise<void>;
|
|
139
141
|
isSaving: boolean;
|
|
142
|
+
searchOpen?: boolean;
|
|
143
|
+
onSearchOpenChange?: (open: boolean) => void;
|
|
140
144
|
disabled?: boolean;
|
|
141
145
|
}
|
|
142
146
|
|
|
@@ -168,6 +172,8 @@ export interface DependenciesSectionProps {
|
|
|
168
172
|
onNavigateToTask: (taskId: string) => void;
|
|
169
173
|
onAddBlockingTaskDialog?: () => void; // Opens dialog to create new blocking task
|
|
170
174
|
onAddBlockedByTaskDialog?: () => void; // Opens dialog to create new blocked-by task
|
|
175
|
+
searchOpen?: boolean;
|
|
176
|
+
onSearchOpenChange?: (open: boolean) => void;
|
|
171
177
|
disabled?: boolean;
|
|
172
178
|
}
|
|
173
179
|
|
|
@@ -182,5 +188,7 @@ export interface RelatedSectionProps {
|
|
|
182
188
|
onRemoveRelated: (taskId: string) => void;
|
|
183
189
|
onNavigateToTask: (taskId: string) => void;
|
|
184
190
|
onAddRelatedTaskDialog?: () => void; // Opens dialog to create new related task;
|
|
191
|
+
searchOpen?: boolean;
|
|
192
|
+
onSearchOpenChange?: (open: boolean) => void;
|
|
185
193
|
disabled?: boolean;
|
|
186
194
|
}
|
|
@@ -48,6 +48,8 @@ interface TaskDialogActionsProps {
|
|
|
48
48
|
onOpenShareDialog?: () => void;
|
|
49
49
|
disabled?: boolean;
|
|
50
50
|
controlsDisabled?: boolean;
|
|
51
|
+
moreMenuOpen?: boolean;
|
|
52
|
+
onMoreMenuOpenChange?: (open: boolean) => void;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
export function TaskDialogActions({
|
|
@@ -66,10 +68,15 @@ export function TaskDialogActions({
|
|
|
66
68
|
onOpenShareDialog,
|
|
67
69
|
disabled = false,
|
|
68
70
|
controlsDisabled = false,
|
|
71
|
+
moreMenuOpen,
|
|
72
|
+
onMoreMenuOpenChange,
|
|
69
73
|
}: TaskDialogActionsProps) {
|
|
70
74
|
const t = useTranslations();
|
|
71
75
|
const tasksHref = useTasksHref();
|
|
72
|
-
const [
|
|
76
|
+
const [uncontrolledMoreMenuOpen, setUncontrolledMoreMenuOpen] =
|
|
77
|
+
useState(false);
|
|
78
|
+
const isMoreMenuOpen = moreMenuOpen ?? uncontrolledMoreMenuOpen;
|
|
79
|
+
const setIsMoreMenuOpen = onMoreMenuOpenChange ?? setUncontrolledMoreMenuOpen;
|
|
73
80
|
|
|
74
81
|
// Determine if we should show the back button (create mode with a pending relationship)
|
|
75
82
|
const showBackButton = isCreateMode && onNavigateBack && navigateBackTaskName;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import '@testing-library/jest-dom';
|
|
6
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
7
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
8
|
+
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { TaskPropertiesSection } from './task-properties-section';
|
|
11
|
+
|
|
12
|
+
vi.mock('next-intl', () => ({
|
|
13
|
+
useTranslations: () => (key: string) => key,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('@tuturuuu/ui/date-time-picker', () => ({
|
|
17
|
+
DateTimePicker: () => <button type="button">date-time-picker</button>,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('@tuturuuu/ui/hooks/use-calendar-preferences', () => ({
|
|
21
|
+
useCalendarPreferences: () => ({
|
|
22
|
+
weekStartsOn: 1,
|
|
23
|
+
timezone: 'UTC',
|
|
24
|
+
timeFormat: '24h',
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
function renderTaskPropertiesSection() {
|
|
29
|
+
const props = {
|
|
30
|
+
wsId: 'ws-1',
|
|
31
|
+
boardId: 'board-1',
|
|
32
|
+
taskId: 'task-1',
|
|
33
|
+
priority: null,
|
|
34
|
+
startDate: undefined,
|
|
35
|
+
endDate: undefined,
|
|
36
|
+
estimationPoints: null,
|
|
37
|
+
selectedLabels: [],
|
|
38
|
+
selectedProjects: [],
|
|
39
|
+
selectedListId: 'list-1',
|
|
40
|
+
selectedAssignees: [],
|
|
41
|
+
isLoading: false,
|
|
42
|
+
isPersonalWorkspace: false,
|
|
43
|
+
canUseBoardAssignees: true,
|
|
44
|
+
isCreateMode: false,
|
|
45
|
+
totalDuration: null,
|
|
46
|
+
isSplittable: false,
|
|
47
|
+
minSplitDurationMinutes: null,
|
|
48
|
+
maxSplitDurationMinutes: null,
|
|
49
|
+
calendarHours: null,
|
|
50
|
+
autoSchedule: false,
|
|
51
|
+
savedSchedulingSettings: undefined,
|
|
52
|
+
scheduledEvents: [],
|
|
53
|
+
availableLists: [
|
|
54
|
+
{
|
|
55
|
+
id: 'list-1',
|
|
56
|
+
board_id: 'board-1',
|
|
57
|
+
name: 'To Do',
|
|
58
|
+
status: 'not_started',
|
|
59
|
+
color: 'GRAY',
|
|
60
|
+
position: 0,
|
|
61
|
+
archived: false,
|
|
62
|
+
deleted: false,
|
|
63
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
64
|
+
creator_id: 'user-1',
|
|
65
|
+
},
|
|
66
|
+
] satisfies TaskList[],
|
|
67
|
+
availableLabels: [
|
|
68
|
+
{
|
|
69
|
+
id: 'label-1',
|
|
70
|
+
name: 'Bug',
|
|
71
|
+
color: '#ef4444',
|
|
72
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
taskProjects: [],
|
|
76
|
+
workspaceMembers: [
|
|
77
|
+
{
|
|
78
|
+
id: 'user-1',
|
|
79
|
+
user_id: 'user-1',
|
|
80
|
+
display_name: 'Taylor',
|
|
81
|
+
email: 'taylor@example.com',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
boardConfig: {},
|
|
85
|
+
onPriorityChange: vi.fn(),
|
|
86
|
+
onStartDateChange: vi.fn(),
|
|
87
|
+
onEndDateChange: vi.fn(),
|
|
88
|
+
onEstimationChange: vi.fn(),
|
|
89
|
+
onLabelToggle: vi.fn(),
|
|
90
|
+
onProjectToggle: vi.fn(),
|
|
91
|
+
onListChange: vi.fn(),
|
|
92
|
+
onAssigneeToggle: vi.fn(),
|
|
93
|
+
onQuickDueDate: vi.fn(),
|
|
94
|
+
onShowNewLabelDialog: vi.fn(),
|
|
95
|
+
onShowNewProjectDialog: vi.fn(),
|
|
96
|
+
onShowEstimationConfigDialog: vi.fn(),
|
|
97
|
+
onTotalDurationChange: vi.fn(),
|
|
98
|
+
onIsSplittableChange: vi.fn(),
|
|
99
|
+
onMinSplitDurationChange: vi.fn(),
|
|
100
|
+
onMaxSplitDurationChange: vi.fn(),
|
|
101
|
+
onCalendarHoursChange: vi.fn(),
|
|
102
|
+
onAutoScheduleChange: vi.fn(),
|
|
103
|
+
onSaveSchedulingSettings: vi.fn().mockResolvedValue(true),
|
|
104
|
+
schedulingSaving: false,
|
|
105
|
+
disabled: false,
|
|
106
|
+
isDraftMode: false,
|
|
107
|
+
variant: 'compact' as const,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const queryClient = new QueryClient({
|
|
111
|
+
defaultOptions: {
|
|
112
|
+
queries: { retry: false },
|
|
113
|
+
mutations: { retry: false },
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
render(
|
|
118
|
+
<QueryClientProvider client={queryClient}>
|
|
119
|
+
<TaskPropertiesSection {...props} />
|
|
120
|
+
</QueryClientProvider>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return props;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
describe('TaskPropertiesSection', () => {
|
|
127
|
+
it('keeps only one property popover open at a time', () => {
|
|
128
|
+
renderTaskPropertiesSection();
|
|
129
|
+
|
|
130
|
+
fireEvent.click(screen.getByLabelText('common.priority'));
|
|
131
|
+
expect(screen.getByText('tasks.priority_critical')).toBeInTheDocument();
|
|
132
|
+
|
|
133
|
+
fireEvent.click(screen.getByLabelText('common.labels'));
|
|
134
|
+
expect(
|
|
135
|
+
screen.queryByText('tasks.priority_critical')
|
|
136
|
+
).not.toBeInTheDocument();
|
|
137
|
+
expect(
|
|
138
|
+
screen.getByPlaceholderText('common.search_labels')
|
|
139
|
+
).toBeInTheDocument();
|
|
140
|
+
|
|
141
|
+
fireEvent.click(screen.getByLabelText('common.list_name_to_do'));
|
|
142
|
+
expect(
|
|
143
|
+
screen.queryByPlaceholderText('common.search_labels')
|
|
144
|
+
).not.toBeInTheDocument();
|
|
145
|
+
expect(screen.getByLabelText('common.list_name_to_do')).toHaveAttribute(
|
|
146
|
+
'aria-expanded',
|
|
147
|
+
'true'
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -102,6 +102,7 @@ interface TaskPropertiesSectionProps {
|
|
|
102
102
|
selectedAssignees: any[];
|
|
103
103
|
isLoading: boolean;
|
|
104
104
|
isPersonalWorkspace: boolean;
|
|
105
|
+
canUseBoardAssignees?: boolean;
|
|
105
106
|
isCreateMode: boolean;
|
|
106
107
|
// Scheduling state
|
|
107
108
|
totalDuration: number | null;
|
|
@@ -151,6 +152,16 @@ interface TaskPropertiesSectionProps {
|
|
|
151
152
|
variant?: 'default' | 'compact';
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
type TaskPropertyPopoverId =
|
|
156
|
+
| 'priority'
|
|
157
|
+
| 'list'
|
|
158
|
+
| 'dates'
|
|
159
|
+
| 'estimation'
|
|
160
|
+
| 'labels'
|
|
161
|
+
| 'projects'
|
|
162
|
+
| 'assignees'
|
|
163
|
+
| 'scheduling';
|
|
164
|
+
|
|
154
165
|
function TaskPropertyPopoverTrigger({
|
|
155
166
|
children,
|
|
156
167
|
compact,
|
|
@@ -320,6 +331,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
320
331
|
selectedAssignees,
|
|
321
332
|
isLoading,
|
|
322
333
|
isPersonalWorkspace,
|
|
334
|
+
canUseBoardAssignees = !isPersonalWorkspace,
|
|
323
335
|
isCreateMode,
|
|
324
336
|
// Scheduling state
|
|
325
337
|
totalDuration,
|
|
@@ -399,17 +411,27 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
399
411
|
const { weekStartsOn, timezone, timeFormat } = useCalendarPreferences();
|
|
400
412
|
|
|
401
413
|
const [isMetadataExpanded, setIsMetadataExpanded] = useState(false);
|
|
402
|
-
const [
|
|
403
|
-
|
|
404
|
-
const [isEstimationPopoverOpen, setIsEstimationPopoverOpen] = useState(false);
|
|
405
|
-
const [isLabelsPopoverOpen, setIsLabelsPopoverOpen] = useState(false);
|
|
406
|
-
const [isProjectsPopoverOpen, setIsProjectsPopoverOpen] = useState(false);
|
|
407
|
-
const [isAssigneesPopoverOpen, setIsAssigneesPopoverOpen] = useState(false);
|
|
408
|
-
const [isSchedulingPopoverOpen, setIsSchedulingPopoverOpen] = useState(false);
|
|
414
|
+
const [activePopover, setActivePopover] =
|
|
415
|
+
useState<TaskPropertyPopoverId | null>(null);
|
|
409
416
|
const [labelSearchQuery, setLabelSearchQuery] = useState('');
|
|
410
417
|
const [projectSearchQuery, setProjectSearchQuery] = useState('');
|
|
411
418
|
const [assigneeSearchQuery, setAssigneeSearchQuery] = useState('');
|
|
412
419
|
|
|
420
|
+
const isPopoverOpen = useCallback(
|
|
421
|
+
(popoverId: TaskPropertyPopoverId) => activePopover === popoverId,
|
|
422
|
+
[activePopover]
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const setPopoverOpen = useCallback(
|
|
426
|
+
(popoverId: TaskPropertyPopoverId, open: boolean) => {
|
|
427
|
+
setActivePopover((currentPopover) => {
|
|
428
|
+
if (open) return popoverId;
|
|
429
|
+
return currentPopover === popoverId ? null : currentPopover;
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
[]
|
|
433
|
+
);
|
|
434
|
+
|
|
413
435
|
const unselectedAvailableLabels = useMemo(
|
|
414
436
|
() =>
|
|
415
437
|
availableLabels.filter(
|
|
@@ -551,7 +573,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
551
573
|
if (success) {
|
|
552
574
|
// Update local saved state to reflect the successful save
|
|
553
575
|
setLastSavedSettings(settings);
|
|
554
|
-
|
|
576
|
+
setPopoverOpen('scheduling', false);
|
|
555
577
|
}
|
|
556
578
|
}, [
|
|
557
579
|
totalDuration,
|
|
@@ -561,6 +583,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
561
583
|
calendarHours,
|
|
562
584
|
autoSchedule,
|
|
563
585
|
onSaveSchedulingSettings,
|
|
586
|
+
setPopoverOpen,
|
|
564
587
|
]);
|
|
565
588
|
|
|
566
589
|
// Handle clear and save scheduling settings
|
|
@@ -585,7 +608,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
585
608
|
const success = await onSaveSchedulingSettings(clearedSettings);
|
|
586
609
|
if (success) {
|
|
587
610
|
setLastSavedSettings(clearedSettings);
|
|
588
|
-
|
|
611
|
+
setPopoverOpen('scheduling', false);
|
|
589
612
|
}
|
|
590
613
|
}, [
|
|
591
614
|
onTotalDurationChange,
|
|
@@ -595,6 +618,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
595
618
|
onCalendarHoursChange,
|
|
596
619
|
onAutoScheduleChange,
|
|
597
620
|
onSaveSchedulingSettings,
|
|
621
|
+
setPopoverOpen,
|
|
598
622
|
]);
|
|
599
623
|
|
|
600
624
|
// Note: Manual scheduling removed - handled by Smart Schedule button in Calendar
|
|
@@ -813,7 +837,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
813
837
|
})}
|
|
814
838
|
</Badge>
|
|
815
839
|
)}
|
|
816
|
-
{selectedAssignees.length > 0 &&
|
|
840
|
+
{selectedAssignees.length > 0 && canUseBoardAssignees && (
|
|
817
841
|
<Badge
|
|
818
842
|
variant="secondary"
|
|
819
843
|
className="h-5 shrink-0 gap-1 border border-dynamic-cyan/30 bg-dynamic-cyan/15 px-2 font-medium text-[10px] text-dynamic-cyan"
|
|
@@ -869,8 +893,8 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
869
893
|
>
|
|
870
894
|
{/* Priority Badge */}
|
|
871
895
|
<Popover
|
|
872
|
-
open={
|
|
873
|
-
onOpenChange={
|
|
896
|
+
open={isPopoverOpen('priority')}
|
|
897
|
+
onOpenChange={(open) => setPopoverOpen('priority', open)}
|
|
874
898
|
>
|
|
875
899
|
<TaskPropertyPopoverTrigger
|
|
876
900
|
compact={isCompact}
|
|
@@ -937,7 +961,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
937
961
|
type="button"
|
|
938
962
|
onClick={() => {
|
|
939
963
|
onPriorityChange(opt.value as TaskPriority);
|
|
940
|
-
|
|
964
|
+
setPopoverOpen('priority', false);
|
|
941
965
|
}}
|
|
942
966
|
className={cn(
|
|
943
967
|
'flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-left text-sm transition-colors hover:bg-muted',
|
|
@@ -959,7 +983,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
959
983
|
label={t('ws-task-boards.dialog.clear_priority')}
|
|
960
984
|
onClick={() => {
|
|
961
985
|
onPriorityChange(null);
|
|
962
|
-
|
|
986
|
+
setPopoverOpen('priority', false);
|
|
963
987
|
}}
|
|
964
988
|
/>
|
|
965
989
|
)}
|
|
@@ -974,13 +998,15 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
974
998
|
availableLists={availableLists}
|
|
975
999
|
disabled={disabled}
|
|
976
1000
|
compact={isCompact}
|
|
1001
|
+
open={isPopoverOpen('list')}
|
|
1002
|
+
onOpenChange={(open) => setPopoverOpen('list', open)}
|
|
977
1003
|
onListChange={onListChange}
|
|
978
1004
|
/>
|
|
979
1005
|
|
|
980
1006
|
{/* Dates Badge */}
|
|
981
1007
|
<Popover
|
|
982
|
-
open={
|
|
983
|
-
onOpenChange={
|
|
1008
|
+
open={isPopoverOpen('dates')}
|
|
1009
|
+
onOpenChange={(open) => setPopoverOpen('dates', open)}
|
|
984
1010
|
>
|
|
985
1011
|
<TaskPropertyPopoverTrigger
|
|
986
1012
|
compact={isCompact}
|
|
@@ -1126,8 +1152,8 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1126
1152
|
|
|
1127
1153
|
{/* Estimation Points Badge */}
|
|
1128
1154
|
<Popover
|
|
1129
|
-
open={
|
|
1130
|
-
onOpenChange={
|
|
1155
|
+
open={isPopoverOpen('estimation')}
|
|
1156
|
+
onOpenChange={(open) => setPopoverOpen('estimation', open)}
|
|
1131
1157
|
>
|
|
1132
1158
|
<TaskPropertyPopoverTrigger
|
|
1133
1159
|
compact={isCompact}
|
|
@@ -1175,7 +1201,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1175
1201
|
actionLabel={t('common.configure')}
|
|
1176
1202
|
ActionIcon={Pen}
|
|
1177
1203
|
onAction={() => {
|
|
1178
|
-
|
|
1204
|
+
setPopoverOpen('estimation', false);
|
|
1179
1205
|
onShowEstimationConfigDialog();
|
|
1180
1206
|
}}
|
|
1181
1207
|
/>
|
|
@@ -1187,7 +1213,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1187
1213
|
type="button"
|
|
1188
1214
|
onClick={() => {
|
|
1189
1215
|
onEstimationChange(idx);
|
|
1190
|
-
|
|
1216
|
+
setPopoverOpen('estimation', false);
|
|
1191
1217
|
}}
|
|
1192
1218
|
className={cn(
|
|
1193
1219
|
'flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm transition-colors hover:bg-muted',
|
|
@@ -1211,7 +1237,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1211
1237
|
label={t('ws-task-boards.dialog.clear_estimate')}
|
|
1212
1238
|
onClick={() => {
|
|
1213
1239
|
onEstimationChange(null);
|
|
1214
|
-
|
|
1240
|
+
setPopoverOpen('estimation', false);
|
|
1215
1241
|
}}
|
|
1216
1242
|
/>
|
|
1217
1243
|
)}
|
|
@@ -1222,9 +1248,9 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1222
1248
|
|
|
1223
1249
|
{/* Labels Badge */}
|
|
1224
1250
|
<Popover
|
|
1225
|
-
open={
|
|
1251
|
+
open={isPopoverOpen('labels')}
|
|
1226
1252
|
onOpenChange={(open) => {
|
|
1227
|
-
|
|
1253
|
+
setPopoverOpen('labels', open);
|
|
1228
1254
|
if (!open) setLabelSearchQuery('');
|
|
1229
1255
|
}}
|
|
1230
1256
|
>
|
|
@@ -1274,7 +1300,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1274
1300
|
actionLabel={t('ws-task-boards.dialog.create_label')}
|
|
1275
1301
|
ActionIcon={Plus}
|
|
1276
1302
|
onAction={() => {
|
|
1277
|
-
|
|
1303
|
+
setPopoverOpen('labels', false);
|
|
1278
1304
|
onShowNewLabelDialog();
|
|
1279
1305
|
}}
|
|
1280
1306
|
/>
|
|
@@ -1355,7 +1381,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1355
1381
|
size="sm"
|
|
1356
1382
|
variant="ghost"
|
|
1357
1383
|
onClick={() => {
|
|
1358
|
-
|
|
1384
|
+
setPopoverOpen('labels', false);
|
|
1359
1385
|
onShowNewLabelDialog();
|
|
1360
1386
|
}}
|
|
1361
1387
|
className="h-8 w-full justify-start"
|
|
@@ -1372,9 +1398,9 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1372
1398
|
{/* Projects Badge — not available for drafts */}
|
|
1373
1399
|
{!isDraftMode && (
|
|
1374
1400
|
<Popover
|
|
1375
|
-
open={
|
|
1401
|
+
open={isPopoverOpen('projects')}
|
|
1376
1402
|
onOpenChange={(open) => {
|
|
1377
|
-
|
|
1403
|
+
setPopoverOpen('projects', open);
|
|
1378
1404
|
if (!open) setProjectSearchQuery('');
|
|
1379
1405
|
}}
|
|
1380
1406
|
>
|
|
@@ -1424,7 +1450,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1424
1450
|
actionLabel={t('ws-task-boards.dialog.create_project')}
|
|
1425
1451
|
ActionIcon={Plus}
|
|
1426
1452
|
onAction={() => {
|
|
1427
|
-
|
|
1453
|
+
setPopoverOpen('projects', false);
|
|
1428
1454
|
onShowNewProjectDialog();
|
|
1429
1455
|
}}
|
|
1430
1456
|
/>
|
|
@@ -1494,7 +1520,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1494
1520
|
size="sm"
|
|
1495
1521
|
variant="ghost"
|
|
1496
1522
|
onClick={() => {
|
|
1497
|
-
|
|
1523
|
+
setPopoverOpen('projects', false);
|
|
1498
1524
|
onShowNewProjectDialog();
|
|
1499
1525
|
}}
|
|
1500
1526
|
className="h-8 w-full justify-start"
|
|
@@ -1510,11 +1536,11 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1510
1536
|
)}
|
|
1511
1537
|
|
|
1512
1538
|
{/* Assignees Badge */}
|
|
1513
|
-
{
|
|
1539
|
+
{canUseBoardAssignees && (
|
|
1514
1540
|
<Popover
|
|
1515
|
-
open={
|
|
1541
|
+
open={isPopoverOpen('assignees')}
|
|
1516
1542
|
onOpenChange={(open) => {
|
|
1517
|
-
|
|
1543
|
+
setPopoverOpen('assignees', open);
|
|
1518
1544
|
if (!open) setAssigneeSearchQuery('');
|
|
1519
1545
|
}}
|
|
1520
1546
|
>
|
|
@@ -1643,8 +1669,8 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1643
1669
|
{/* Scheduling Badge — not available for drafts */}
|
|
1644
1670
|
{!isDraftMode && (
|
|
1645
1671
|
<Popover
|
|
1646
|
-
open={
|
|
1647
|
-
onOpenChange={
|
|
1672
|
+
open={isPopoverOpen('scheduling')}
|
|
1673
|
+
onOpenChange={(open) => setPopoverOpen('scheduling', open)}
|
|
1648
1674
|
>
|
|
1649
1675
|
<TaskPropertyPopoverTrigger
|
|
1650
1676
|
compact={isCompact}
|
|
@@ -2006,7 +2032,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
2006
2032
|
label={t('ws-task-boards.dialog.clear_duration')}
|
|
2007
2033
|
onClick={() => {
|
|
2008
2034
|
onTotalDurationChange(null);
|
|
2009
|
-
|
|
2035
|
+
setPopoverOpen('scheduling', false);
|
|
2010
2036
|
}}
|
|
2011
2037
|
/>
|
|
2012
2038
|
)}
|
|
@@ -22,6 +22,8 @@ import type {
|
|
|
22
22
|
TaskRelationshipsPropertiesProps,
|
|
23
23
|
} from './relationships/types/task-relationships.types';
|
|
24
24
|
|
|
25
|
+
type RelationshipSearchPopoverId = RelationshipTab;
|
|
26
|
+
|
|
25
27
|
export function TaskRelationshipsProperties({
|
|
26
28
|
wsId,
|
|
27
29
|
taskId,
|
|
@@ -59,6 +61,24 @@ export function TaskRelationshipsProperties({
|
|
|
59
61
|
const [activeTab, setActiveTab] = React.useState<RelationshipTab>(
|
|
60
62
|
initialActiveTab ?? 'parent'
|
|
61
63
|
);
|
|
64
|
+
const [activeSearchPopover, setActiveSearchPopover] =
|
|
65
|
+
React.useState<RelationshipSearchPopoverId | null>(null);
|
|
66
|
+
|
|
67
|
+
const isSearchPopoverOpen = React.useCallback(
|
|
68
|
+
(popoverId: RelationshipSearchPopoverId) =>
|
|
69
|
+
activeSearchPopover === popoverId,
|
|
70
|
+
[activeSearchPopover]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const setSearchPopoverOpen = React.useCallback(
|
|
74
|
+
(popoverId: RelationshipSearchPopoverId, open: boolean) => {
|
|
75
|
+
setActiveSearchPopover((currentPopover) => {
|
|
76
|
+
if (open) return popoverId;
|
|
77
|
+
return currentPopover === popoverId ? null : currentPopover;
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
[]
|
|
81
|
+
);
|
|
62
82
|
|
|
63
83
|
// Tab configuration
|
|
64
84
|
const tabs = React.useMemo(
|
|
@@ -111,7 +131,10 @@ export function TaskRelationshipsProperties({
|
|
|
111
131
|
{/* Header with toggle button */}
|
|
112
132
|
<button
|
|
113
133
|
type="button"
|
|
114
|
-
onClick={() =>
|
|
134
|
+
onClick={() => {
|
|
135
|
+
setIsExpanded((current) => !current);
|
|
136
|
+
setActiveSearchPopover(null);
|
|
137
|
+
}}
|
|
115
138
|
className="flex w-full items-center justify-between px-4 py-3 text-left transition-colors hover:bg-background/40 md:px-8"
|
|
116
139
|
>
|
|
117
140
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
@@ -185,7 +208,10 @@ export function TaskRelationshipsProperties({
|
|
|
185
208
|
<TabButton
|
|
186
209
|
key={tab.id}
|
|
187
210
|
active={activeTab === tab.id}
|
|
188
|
-
onClick={() =>
|
|
211
|
+
onClick={() => {
|
|
212
|
+
setActiveTab(tab.id);
|
|
213
|
+
setActiveSearchPopover(null);
|
|
214
|
+
}}
|
|
189
215
|
icon={tab.icon}
|
|
190
216
|
label={tab.label}
|
|
191
217
|
count={tab.count}
|
|
@@ -208,6 +234,10 @@ export function TaskRelationshipsProperties({
|
|
|
208
234
|
onRemoveParent={onRemoveParent}
|
|
209
235
|
onNavigateToTask={onNavigateToTask}
|
|
210
236
|
onAddParentTask={onAddParentTask}
|
|
237
|
+
searchOpen={isSearchPopoverOpen('parent')}
|
|
238
|
+
onSearchOpenChange={(open) =>
|
|
239
|
+
setSearchPopoverOpen('parent', open)
|
|
240
|
+
}
|
|
211
241
|
disabled={disabled}
|
|
212
242
|
/>
|
|
213
243
|
)}
|
|
@@ -224,6 +254,10 @@ export function TaskRelationshipsProperties({
|
|
|
224
254
|
onAddSubtask={onAddSubtask}
|
|
225
255
|
onAddExistingAsSubtask={onAddExistingAsSubtask}
|
|
226
256
|
isSaving={isSaving}
|
|
257
|
+
searchOpen={isSearchPopoverOpen('subtasks')}
|
|
258
|
+
onSearchOpenChange={(open) =>
|
|
259
|
+
setSearchPopoverOpen('subtasks', open)
|
|
260
|
+
}
|
|
227
261
|
disabled={disabled}
|
|
228
262
|
/>
|
|
229
263
|
)}
|
|
@@ -244,6 +278,10 @@ export function TaskRelationshipsProperties({
|
|
|
244
278
|
onNavigateToTask={onNavigateToTask}
|
|
245
279
|
onAddBlockingTaskDialog={onAddBlockingTaskDialog}
|
|
246
280
|
onAddBlockedByTaskDialog={onAddBlockedByTaskDialog}
|
|
281
|
+
searchOpen={isSearchPopoverOpen('dependencies')}
|
|
282
|
+
onSearchOpenChange={(open) =>
|
|
283
|
+
setSearchPopoverOpen('dependencies', open)
|
|
284
|
+
}
|
|
247
285
|
disabled={disabled}
|
|
248
286
|
/>
|
|
249
287
|
)}
|
|
@@ -259,6 +297,10 @@ export function TaskRelationshipsProperties({
|
|
|
259
297
|
onRemoveRelated={onRemoveRelatedTask}
|
|
260
298
|
onNavigateToTask={onNavigateToTask}
|
|
261
299
|
onAddRelatedTaskDialog={onAddRelatedTaskDialog}
|
|
300
|
+
searchOpen={isSearchPopoverOpen('related')}
|
|
301
|
+
onSearchOpenChange={(open) =>
|
|
302
|
+
setSearchPopoverOpen('related', open)
|
|
303
|
+
}
|
|
262
304
|
disabled={disabled}
|
|
263
305
|
/>
|
|
264
306
|
)}
|