feat(tasks): require explicit board selection

This commit is contained in:
lizerk 2026-08-07 20:02:39 +03:00
parent 205b4d6492
commit 51cbb419b5
3 changed files with 35 additions and 113 deletions

View File

@ -57,7 +57,6 @@ import {
Rows3,
Search,
ShoppingCart,
Star,
Tag,
Trash2,
UserPlus,
@ -3924,9 +3923,15 @@ function TasksPageContent() {
const loadBoard = useCallback(async (boardId?: number | null) => {
setError(null);
const data = boardId
? await apiClient.getTaskBoard(boardId)
: await apiClient.getDefaultTaskBoard();
if (!boardId) {
setBoard(null);
setSelectedBoardId(null);
boardViewStateRef.current = EMPTY_BOARD_VIEW_STATE;
setBoardViewState(EMPTY_BOARD_VIEW_STATE);
setViewTaskId(null);
return null;
}
const data = await apiClient.getTaskBoard(boardId);
setBoard(data);
setTaskLabels(data.labels || []);
let nextViewState = normalizeBoardViewState(data.view_state);
@ -4300,15 +4305,6 @@ function TasksPageContent() {
const boardList = await loadBoards();
if (!currentBoardId) {
const nextBoard = eventBoardId
? boardList.find((item) => item.id === eventBoardId)
: boardList[0];
if (nextBoard) {
await loadBoard(nextBoard.id);
} else {
await loadBoard(null);
await loadBoards();
}
return;
}
@ -4316,12 +4312,8 @@ function TasksPageContent() {
(item) => item.id === currentBoardId,
);
if (!currentBoardAvailable) {
if (boardList[0]) {
await loadBoard(boardList[0].id);
} else {
await loadBoard(null);
await loadBoards();
}
await loadBoard(null);
router.replace("/tasks", { scroll: false });
setSearch("");
setOnlyMine(false);
setSelectedColumnTarget("all");
@ -4334,7 +4326,7 @@ function TasksPageContent() {
} catch (syncError) {
setError(getTaskError(syncError, "Не удалось синхронизировать доску"));
}
}, [loadBoard, loadBoards]);
}, [loadBoard, loadBoards, router]);
useTaskBoardRealtime({
viewTaskId,
@ -4372,7 +4364,7 @@ function TasksPageContent() {
] = await Promise.all([
requestedBoardId > 0
? apiClient.getTaskBoard(requestedBoardId)
: apiClient.getDefaultTaskBoard(),
: Promise.resolve(null),
apiClient.getTaskBoards({ view: "summary" }),
apiClient.getTaskBoardGroups(),
apiClient.getEmployees({ limit: 200, is_active: true, ordering: "last_name" }),
@ -4387,11 +4379,13 @@ function TasksPageContent() {
setBoards(boardList as TaskBoardSummary[]);
setBoardGroups(groupList as TaskBoardGroup[]);
setBoard(boardData);
const initialViewState = normalizeBoardViewState(boardData.view_state);
const initialViewState = boardData
? normalizeBoardViewState(boardData.view_state)
: EMPTY_BOARD_VIEW_STATE;
boardViewStateRef.current = initialViewState;
setBoardViewState(initialViewState);
setSelectedBoardId(boardData.id);
if (window.matchMedia("(max-width: 1023px)").matches) {
setSelectedBoardId(boardData?.id ?? null);
if (boardData && window.matchMedia("(max-width: 1023px)").matches) {
const firstColumn = (boardData.columns || []).find(
(column: TaskColumn) => !column.is_archived && !column.parent,
);
@ -4401,6 +4395,7 @@ function TasksPageContent() {
setDepartments(departmentsData.results || departmentsData || []);
setTaskLabels(Array.isArray(labelsData) ? labelsData : labelsData.results || []);
if (
boardData &&
requestedTaskId > 0 &&
(boardData.tasks || []).some((task: TaskCard) => task.id === requestedTaskId)
) {
@ -5394,35 +5389,6 @@ function TasksPageContent() {
}
}, [board, loadBoard, loadBoards]);
const setDefaultBoard = useCallback(async (
targetBoard: Pick<TaskBoardSummary, "id" | "is_default_for_current_user">,
) => {
if (saving || targetBoard.is_default_for_current_user) return;
setBoardActionsMenuOpen(false);
setSaving(true);
setError(null);
try {
const updatedBoard = await apiClient.setDefaultTaskBoard(targetBoard.id);
setBoard((current) => (
current
? {
...current,
is_default_for_current_user: current.id === updatedBoard.id,
}
: current
));
setBoards((current) => current.map((item) => ({
...item,
is_default_for_current_user: item.id === updatedBoard.id,
})));
} catch (defaultError) {
setError(getTaskError(defaultError, "Не удалось выбрать доску по умолчанию"));
} finally {
setSaving(false);
}
}, [saving]);
const deleteBoard = useCallback(async (
targetBoard: Pick<TaskBoardSummary, "id" | "name">,
) => {
@ -5434,16 +5400,10 @@ function TasksPageContent() {
setError(null);
try {
await apiClient.deleteTaskBoard(targetBoard.id);
const nextBoards = await loadBoards();
await loadBoards();
if (selectedBoardId === targetBoard.id) {
const nextBoard = nextBoards[0] || null;
if (nextBoard) {
router.replace(`/tasks?board=${nextBoard.id}`, { scroll: false });
} else {
setBoard(null);
setSelectedBoardId(null);
router.replace("/tasks", { scroll: false });
}
await loadBoard(null);
router.replace("/tasks", { scroll: false });
setSearch("");
setOnlyMine(false);
setSelectedColumnTarget("all");
@ -5453,7 +5413,7 @@ function TasksPageContent() {
} finally {
setSaving(false);
}
}, [loadBoards, router, saving, selectedBoardId]);
}, [loadBoard, loadBoards, router, saving, selectedBoardId]);
const deleteSelectedBoard = useCallback(() => {
if (!board) return;
@ -7411,7 +7371,6 @@ function TasksPageContent() {
onToggleHiddenGroup={(group) => void toggleBoardGroupHidden(group)}
onEditBoard={openEditBoard}
onDeleteBoard={(targetBoard) => void deleteBoard(targetBoard)}
onSetDefaultBoard={(targetBoard) => void setDefaultBoard(targetBoard)}
onToggleBoardPin={(targetBoard) => void toggleBoardPin(targetBoard)}
onToggleBoardNotifications={(targetBoard) => void toggleBoardNotifications(targetBoard)}
onMoveBoard={setBoardGroupMemberships}
@ -7422,6 +7381,8 @@ function TasksPageContent() {
/>
<div className={`${mobileBoardOpen ? "flex" : "hidden"} relative min-h-0 min-w-0 flex-1 flex-col gap-4 ${desktopWideMode ? "lg:flex lg:h-full" : ""}`}>
{board ? (
<>
<section className="app-surface relative z-50 shrink-0 overflow-visible rounded-2xl p-4">
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
@ -7547,26 +7508,6 @@ function TasksPageContent() {
<Workflow size={14} className="app-text-muted shrink-0" />
<span>Автоматизация</span>
</button>
<button
type="button"
onClick={() => board && void setDefaultBoard(board)}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-[var(--foreground)] transition hover:bg-[var(--surface-secondary)] disabled:cursor-default disabled:opacity-70 disabled:hover:bg-transparent"
disabled={saving || Boolean(board?.is_default_for_current_user)}
>
<Star
size={14}
className={`shrink-0 ${
board?.is_default_for_current_user
? "fill-current app-accent-text"
: "app-text-muted"
}`}
/>
<span>
{board?.is_default_for_current_user
? "Доска по умолчанию"
: "Сделать доской по умолчанию"}
</span>
</button>
<button
type="button"
onClick={() => void deleteSelectedBoard()}
@ -7950,6 +7891,16 @@ function TasksPageContent() {
</button>
</TaskCardMenuPopover>
) : null}
</>
) : (
<section className="app-surface-muted flex h-full min-h-0 flex-1 items-center justify-center rounded-xl border border-dashed border-[var(--border-subtle)]">
<div className="max-w-xs text-center">
<Kanban size={28} className="app-text-muted mx-auto mb-3" />
<p className="text-sm font-semibold text-[var(--foreground)]">Выберите доску</p>
<p className="app-text-muted mt-1 text-xs">Доска откроется справа от списка</p>
</div>
</section>
)}
</div>
</div>
</div>

View File

@ -62,7 +62,6 @@ type TaskBoardNavigatorProps = {
onToggleHiddenGroup: (group: TaskBoardGroup) => void;
onEditBoard: (board: TaskBoardSummary) => void;
onDeleteBoard: (board: TaskBoardSummary) => void;
onSetDefaultBoard: (board: TaskBoardSummary) => void;
onToggleBoardPin: (board: TaskBoardSummary) => void;
onToggleBoardNotifications: (board: TaskBoardSummary) => void;
onMoveBoard: (
@ -102,7 +101,6 @@ function BoardRow({
onToggleMenu,
onEdit,
onDelete,
onSetDefault,
onTogglePin,
onToggleNotifications,
onMove,
@ -118,7 +116,6 @@ function BoardRow({
onToggleMenu: () => void;
onEdit: () => void;
onDelete: () => void;
onSetDefault: () => void;
onTogglePin: () => void;
onToggleNotifications: () => void;
onMove: () => void;
@ -170,9 +167,6 @@ function BoardRow({
<span className="min-w-0 flex-1 truncate text-sm font-semibold text-[var(--foreground)]">
{board.name}
</span>
{board.is_default_for_current_user ? (
<Star size={12} className="app-accent-text shrink-0 fill-current" />
) : null}
{board.is_pinned ? (
<Pin size={12} className="app-accent-text shrink-0 fill-current" />
) : null}
@ -244,20 +238,6 @@ function BoardRow({
/>
{board.is_pinned ? "Открепить" : "Закрепить"}
</button>
<button
type="button"
onClick={onSetDefault}
disabled={board.is_default_for_current_user}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-[var(--foreground)] transition hover:bg-[var(--surface-secondary)] disabled:cursor-default disabled:opacity-70 disabled:hover:bg-transparent"
>
<Star
size={14}
className={board.is_default_for_current_user ? "app-accent-text fill-current" : "app-text-muted"}
/>
{board.is_default_for_current_user
? "Доска по умолчанию"
: "Сделать по умолчанию"}
</button>
<button
type="button"
onClick={onToggleNotifications}
@ -310,7 +290,6 @@ export default function TaskBoardNavigator({
onToggleHiddenGroup,
onEditBoard,
onDeleteBoard,
onSetDefaultBoard,
onToggleBoardPin,
onToggleBoardNotifications,
onMoveBoard,
@ -776,10 +755,6 @@ export default function TaskBoardNavigator({
onDeleteBoard(board);
setBoardMenuId(null);
}}
onSetDefault={() => {
onSetDefaultBoard(board);
setBoardMenuId(null);
}}
onTogglePin={() => {
onToggleBoardPin(board);
setBoardMenuId(null);

View File

@ -43,8 +43,6 @@ export function createTasksApi(request: RequestFn, requestRaw: RawRequestFn) {
return {
getTaskBoards: (params?: Record<string, string | number>) =>
request(`/api/v1/tasks/boards/${buildQuery(params)}`),
getDefaultTaskBoard: (): Promise<TaskBoard> =>
request("/api/v1/tasks/boards/default/"),
getTaskBoard: (id: number): Promise<TaskBoard> =>
request(`/api/v1/tasks/boards/${id}/`),
getTaskBoardParticipantCandidates: (id: number): Promise<User[]> =>
@ -85,8 +83,6 @@ export function createTasksApi(request: RequestFn, requestRaw: RawRequestFn) {
method: "POST",
body: JSON.stringify({ groups }),
}),
setDefaultTaskBoard: (id: number): Promise<TaskBoard> =>
request(`/api/v1/tasks/boards/${id}/set-default/`, { method: "POST" }),
toggleTaskBoardPin: (id: number): Promise<{ ok: boolean; is_pinned: boolean }> =>
request(`/api/v1/tasks/boards/${id}/pin/`, { method: "POST" }),
toggleTaskBoardNotifications: (