feat(tasks): require explicit board selection
This commit is contained in:
parent
205b4d6492
commit
51cbb419b5
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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: (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user