fix task board navigation and completion behavior
This commit is contained in:
parent
a889086661
commit
c945e92a79
@ -170,6 +170,7 @@ class TaskColumnSerializer(serializers.ModelSerializer):
|
||||
raise serializers.ValidationError(
|
||||
{"parent": "Нельзя добавить подколонку в архивную колонку."}
|
||||
)
|
||||
attrs["is_done"] = parent.is_done
|
||||
|
||||
if (
|
||||
self.instance
|
||||
|
||||
@ -633,6 +633,18 @@ class TaskColumnViewSet(viewsets.ModelViewSet):
|
||||
parent = serializer.validated_data.get("parent")
|
||||
if parent:
|
||||
parent = TaskColumn.objects.select_for_update().get(pk=parent.pk)
|
||||
elif serializer.validated_data.get("is_done"):
|
||||
board = serializer.validated_data["board"]
|
||||
TaskColumn.objects.select_for_update().filter(
|
||||
board=board,
|
||||
parent__isnull=True,
|
||||
is_done=True,
|
||||
).update(is_done=False)
|
||||
TaskColumn.objects.filter(
|
||||
board=board,
|
||||
parent__isnull=False,
|
||||
is_done=True,
|
||||
).update(is_done=False)
|
||||
column = serializer.save(parent=parent)
|
||||
if parent:
|
||||
task_updates = {"column": column}
|
||||
@ -644,7 +656,28 @@ class TaskColumnViewSet(viewsets.ModelViewSet):
|
||||
send_task_board_update(column.board, "created", "column", column.id)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
column = serializer.save()
|
||||
with transaction.atomic():
|
||||
current_column = TaskColumn.objects.select_for_update().get(
|
||||
pk=serializer.instance.pk
|
||||
)
|
||||
requested_is_done = serializer.validated_data.get(
|
||||
"is_done",
|
||||
current_column.is_done,
|
||||
)
|
||||
if current_column.parent_id is None and requested_is_done:
|
||||
TaskColumn.objects.filter(
|
||||
board=current_column.board,
|
||||
parent__isnull=True,
|
||||
is_done=True,
|
||||
).exclude(pk=current_column.pk).update(is_done=False)
|
||||
TaskColumn.objects.filter(
|
||||
board=current_column.board,
|
||||
parent__isnull=False,
|
||||
is_done=True,
|
||||
).exclude(parent_id=current_column.pk).update(is_done=False)
|
||||
column = serializer.save()
|
||||
if column.parent_id is None:
|
||||
column.subcolumns.update(is_done=column.is_done)
|
||||
send_task_board_update(column.board, "updated", "column", column.id)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
@ -1247,27 +1280,27 @@ class TaskViewSet(viewsets.ModelViewSet):
|
||||
if task.column.is_done:
|
||||
return Response(self.get_serializer(task).data)
|
||||
|
||||
done_column = (
|
||||
done_parent = (
|
||||
TaskColumn.objects.filter(
|
||||
board_id=task.board_id,
|
||||
is_done=True,
|
||||
is_archived=False,
|
||||
parent__isnull=True,
|
||||
)
|
||||
.annotate(
|
||||
active_subcolumns_count=Count(
|
||||
"subcolumns",
|
||||
filter=Q(subcolumns__is_archived=False),
|
||||
)
|
||||
)
|
||||
.filter(active_subcolumns_count=0)
|
||||
.order_by("position", "id")
|
||||
.first()
|
||||
)
|
||||
if done_column is None:
|
||||
if done_parent is None:
|
||||
return Response(
|
||||
{"detail": "На доске не настроена завершающая колонка."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
done_column = (
|
||||
done_parent.subcolumns.filter(is_archived=False)
|
||||
.order_by("position", "id")
|
||||
.first()
|
||||
or done_parent
|
||||
)
|
||||
|
||||
task = self._move_task(task, done_column, None, None, request.user)
|
||||
return Response(self.get_serializer(task).data)
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def normalize_done_columns(apps, schema_editor):
|
||||
TaskBoard = apps.get_model("tasks", "TaskBoard")
|
||||
TaskColumn = apps.get_model("tasks", "TaskColumn")
|
||||
|
||||
for board_id in TaskBoard.objects.values_list("id", flat=True).iterator():
|
||||
done_columns = TaskColumn.objects.filter(
|
||||
board_id=board_id,
|
||||
parent_id=None,
|
||||
is_done=True,
|
||||
).order_by("-updated_at", "-id")
|
||||
selected_column = done_columns.first()
|
||||
if selected_column is None:
|
||||
continue
|
||||
done_columns.exclude(id=selected_column.id).update(is_done=False)
|
||||
TaskColumn.objects.filter(
|
||||
board_id=board_id,
|
||||
parent_id__isnull=False,
|
||||
).update(is_done=False)
|
||||
TaskColumn.objects.filter(parent_id=selected_column.id).update(is_done=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("tasks", "0027_taskboardplacement_view_state"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(normalize_done_columns, migrations.RunPython.noop),
|
||||
migrations.AddConstraint(
|
||||
model_name="taskcolumn",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(parent__isnull=True, is_done=True),
|
||||
fields=("board",),
|
||||
name="uniq_task_done_column_per_board",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -214,6 +214,11 @@ class TaskColumn(models.Model):
|
||||
condition=models.Q(parent__isnull=False),
|
||||
name="uniq_task_subcolumn_parent_name",
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=["board"],
|
||||
condition=models.Q(parent__isnull=True, is_done=True),
|
||||
name="uniq_task_done_column_per_board",
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@ -1183,7 +1183,7 @@ def test_member_can_claim_unassigned_task(api_client, user):
|
||||
assert completed_task.assignee_id is None
|
||||
|
||||
|
||||
def test_only_assignee_can_complete_task_in_first_done_column(api_client, user):
|
||||
def test_only_assignee_can_complete_task_in_configured_done_column(api_client, user):
|
||||
assignee = make_user("task-assignee@example.com", "+79994440093")
|
||||
member = make_user("task-member@example.com", "+79994440094")
|
||||
board = TaskBoard.objects.create(name="Доска завершения", created_by=user)
|
||||
@ -1193,17 +1193,16 @@ def test_only_assignee_can_complete_task_in_first_done_column(api_client, user):
|
||||
name="В работе",
|
||||
position=1000,
|
||||
)
|
||||
first_done_column = TaskColumn.objects.create(
|
||||
previous_done_column = TaskColumn.objects.create(
|
||||
board=board,
|
||||
name="Готово",
|
||||
position=2000,
|
||||
is_done=True,
|
||||
)
|
||||
TaskColumn.objects.create(
|
||||
configured_done_column = TaskColumn.objects.create(
|
||||
board=board,
|
||||
name="Архив завершенных",
|
||||
name="Опубликовано",
|
||||
position=3000,
|
||||
is_done=True,
|
||||
)
|
||||
task = Task.objects.create(
|
||||
board=board,
|
||||
@ -1214,6 +1213,26 @@ def test_only_assignee_can_complete_task_in_first_done_column(api_client, user):
|
||||
)
|
||||
complete_url = reverse("api:v1:tasks:task-complete", kwargs={"pk": task.id})
|
||||
|
||||
api_client.force_authenticate(user=user)
|
||||
configure_response = api_client.patch(
|
||||
reverse(
|
||||
"api:v1:tasks:task-column-detail",
|
||||
kwargs={"pk": configured_done_column.id},
|
||||
),
|
||||
{"is_done": True},
|
||||
format="json",
|
||||
)
|
||||
assert configure_response.status_code == status.HTTP_200_OK
|
||||
previous_done_column.refresh_from_db()
|
||||
configured_done_column.refresh_from_db()
|
||||
assert previous_done_column.is_done is False
|
||||
assert configured_done_column.is_done is True
|
||||
assert TaskColumn.objects.filter(
|
||||
board=board,
|
||||
parent__isnull=True,
|
||||
is_done=True,
|
||||
).count() == 1
|
||||
|
||||
api_client.force_authenticate(user=member)
|
||||
forbidden_response = api_client.post(complete_url)
|
||||
assert forbidden_response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@ -1229,18 +1248,18 @@ def test_only_assignee_can_complete_task_in_first_done_column(api_client, user):
|
||||
complete_response = api_client.post(complete_url)
|
||||
|
||||
assert complete_response.status_code == status.HTTP_200_OK
|
||||
assert complete_response.data["column"] == first_done_column.id
|
||||
assert complete_response.data["column"] == configured_done_column.id
|
||||
assert complete_response.data["completed_at"] is not None
|
||||
task.refresh_from_db()
|
||||
assert task.column_id == first_done_column.id
|
||||
assert task.column_id == configured_done_column.id
|
||||
assert task.completed_at is not None
|
||||
moved_activity = task.activities.get(action="moved")
|
||||
assert moved_activity.actor_id == assignee.id
|
||||
assert moved_activity.metadata == {
|
||||
"from_column_id": source_column.id,
|
||||
"from_column": source_column.name,
|
||||
"to_column_id": first_done_column.id,
|
||||
"to_column": first_done_column.name,
|
||||
"to_column_id": configured_done_column.id,
|
||||
"to_column": configured_done_column.name,
|
||||
}
|
||||
notify_moved.assert_called_once()
|
||||
send_update.assert_called_once()
|
||||
@ -1257,6 +1276,53 @@ def test_only_assignee_can_complete_task_in_first_done_column(api_client, user):
|
||||
send_update.assert_not_called()
|
||||
|
||||
|
||||
def test_complete_task_uses_first_subcolumn_of_configured_done_column(
|
||||
api_client,
|
||||
user,
|
||||
):
|
||||
board = TaskBoard.objects.create(name="Финал с подколонками", created_by=user)
|
||||
source_column = TaskColumn.objects.create(
|
||||
board=board,
|
||||
name="В работе",
|
||||
position=1000,
|
||||
)
|
||||
done_parent = TaskColumn.objects.create(
|
||||
board=board,
|
||||
name="Завершено",
|
||||
position=2000,
|
||||
is_done=True,
|
||||
)
|
||||
api_client.force_authenticate(user=user)
|
||||
child_response = api_client.post(
|
||||
reverse("api:v1:tasks:task-column-list"),
|
||||
{
|
||||
"board": board.id,
|
||||
"parent": done_parent.id,
|
||||
"name": "Принято",
|
||||
"position": 1000,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
assert child_response.status_code == status.HTTP_201_CREATED
|
||||
done_child = TaskColumn.objects.get(id=child_response.data["id"])
|
||||
assert done_child.is_done is True
|
||||
task = Task.objects.create(
|
||||
board=board,
|
||||
column=source_column,
|
||||
title="Переместить в финальную подколонку",
|
||||
created_by=user,
|
||||
assignee=user,
|
||||
)
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:v1:tasks:task-complete", kwargs={"pk": task.id})
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["column"] == done_child.id
|
||||
assert response.data["completed_at"] is not None
|
||||
|
||||
|
||||
def test_assignee_cannot_complete_task_without_done_column(api_client, user):
|
||||
board = TaskBoard.objects.create(name="Доска без финала", created_by=user)
|
||||
column = TaskColumn.objects.create(board=board, name="В работе", position=1000)
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
DragOverlay,
|
||||
PointerSensor,
|
||||
closestCorners,
|
||||
pointerWithin,
|
||||
useDraggable,
|
||||
useSensor,
|
||||
useSensors,
|
||||
@ -5434,8 +5435,18 @@ function TasksPageContent() {
|
||||
}, []);
|
||||
|
||||
const boardCollisionDetection = useCallback<CollisionDetection>((args) => {
|
||||
if (args.active.data.current?.type !== "column") {
|
||||
const acceptsTasks = (collision: { id: string | number }) => (
|
||||
args.droppableContainers.find(
|
||||
(container) => container.id === collision.id,
|
||||
)?.data.current?.acceptsTasks === true
|
||||
);
|
||||
const pointerCollisions = pointerWithin(args).filter(acceptsTasks);
|
||||
if (pointerCollisions.length > 0) return pointerCollisions;
|
||||
return closestCorners(args).filter(acceptsTasks);
|
||||
}
|
||||
|
||||
const collisions = closestCorners(args);
|
||||
if (args.active.data.current?.type !== "column") return collisions;
|
||||
|
||||
const activeColumnId = Number(args.active.data.current?.columnId);
|
||||
const activeColumn = activeColumns.find((column) => column.id === activeColumnId);
|
||||
@ -5562,8 +5573,8 @@ function TasksPageContent() {
|
||||
desktopWideMode={desktopWideMode}
|
||||
onDesktopWideModeChange={changeDesktopWideMode}
|
||||
>
|
||||
<div className={`${desktopWideMode ? "w-full lg:flex lg:min-h-0 lg:flex-1 lg:flex-col" : "mx-auto w-full max-w-[1600px]"}`}>
|
||||
<div className={`min-h-0 gap-4 ${desktopWideMode ? "lg:flex lg:flex-1" : ""}`}>
|
||||
<div className={`${desktopWideMode ? "w-full lg:flex lg:min-h-0 lg:flex-1 lg:flex-col" : "mx-auto w-full max-w-[1600px] xl:flex xl:h-[calc(100dvh-5.5rem)] xl:min-h-0 xl:flex-col"}`}>
|
||||
<div className={`min-h-0 gap-4 ${desktopWideMode ? "lg:flex lg:flex-1" : "xl:flex xl:flex-1"}`}>
|
||||
<TaskBoardNavigator
|
||||
boards={boards}
|
||||
groups={boardGroups}
|
||||
@ -5587,8 +5598,8 @@ function TasksPageContent() {
|
||||
className={`${mobileBoardOpen ? "hidden" : "flex"} h-[calc(100dvh-7rem)] w-full ${desktopWideMode ? "lg:flex lg:h-full lg:shrink-0" : ""}`}
|
||||
/>
|
||||
|
||||
<div className={`${mobileBoardOpen ? "flex" : "hidden"} relative min-h-0 min-w-0 flex-1 flex-col gap-4 ${desktopWideMode ? "lg:flex lg:h-full" : ""}`}>
|
||||
<section className="app-surface relative z-50 shrink-0 overflow-visible rounded-2xl p-4">
|
||||
<div className={`${mobileBoardOpen ? "flex" : "hidden"} relative min-h-0 min-w-0 flex-1 flex-col gap-4 ${desktopWideMode ? "lg:flex lg:h-full" : "xl:flex xl:h-full"}`}>
|
||||
<section className="app-surface relative z-30 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">
|
||||
<button
|
||||
@ -5827,11 +5838,7 @@ function TasksPageContent() {
|
||||
}}
|
||||
className={`tasks-board-scroll min-w-0 overflow-hidden pb-3 xl:overflow-auto ${
|
||||
boardPanning ? "cursor-grabbing select-none" : ""
|
||||
} ${
|
||||
desktopWideMode
|
||||
? "lg:min-h-0 lg:flex-1"
|
||||
: "xl:max-h-[calc(100dvh-15rem)]"
|
||||
}`}
|
||||
} ${desktopWideMode ? "lg:min-h-0 lg:flex-1" : "xl:min-h-0 xl:flex-1"}`}
|
||||
>
|
||||
<div className="flex w-full min-w-0 gap-3 xl:min-h-full xl:w-auto xl:min-w-max xl:items-stretch">
|
||||
{topLevelColumns.map((column) => {
|
||||
@ -6392,15 +6399,17 @@ function TasksPageContent() {
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="app-surface-muted flex cursor-pointer items-center gap-3 rounded-xl border border-[var(--border-subtle)] px-3 py-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={columnForm.is_done}
|
||||
onChange={(event) => setColumnForm((current) => ({ ...current, is_done: event.target.checked }))}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
Финальная колонка
|
||||
</label>
|
||||
{!columnForm.parent ? (
|
||||
<label className="app-surface-muted flex cursor-pointer items-center gap-3 rounded-xl border border-[var(--border-subtle)] px-3 py-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={columnForm.is_done}
|
||||
onChange={(event) => setColumnForm((current) => ({ ...current, is_done: event.target.checked }))}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
Финальная колонка
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ type MessagesChatListItemProps = {
|
||||
currentUserId?: number;
|
||||
identity: ChatIdentity;
|
||||
active?: boolean;
|
||||
compact?: boolean;
|
||||
onChatUpdated?: (chatId: number) => void | Promise<void>;
|
||||
};
|
||||
|
||||
@ -78,6 +79,7 @@ export default function MessagesChatListItem({
|
||||
currentUserId,
|
||||
identity,
|
||||
active = false,
|
||||
compact = false,
|
||||
onChatUpdated,
|
||||
}: MessagesChatListItemProps) {
|
||||
const title = getChatTitle(chat, currentUserId, identity);
|
||||
@ -132,6 +134,45 @@ export default function MessagesChatListItem({
|
||||
}
|
||||
};
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<Link
|
||||
href={`/messages/${chat.id}`}
|
||||
className={`relative mx-auto flex w-fit rounded-xl p-1.5 transition ${
|
||||
active ? "app-selected" : "hover:bg-[var(--surface-secondary)]"
|
||||
}`}
|
||||
title={title}
|
||||
aria-label={`Открыть чат «${title}»`}
|
||||
aria-current={active ? "page" : undefined}
|
||||
>
|
||||
<span className="relative h-10 w-10 shrink-0 overflow-visible">
|
||||
<span className="app-avatar-fallback flex h-10 w-10 items-center justify-center overflow-hidden rounded-full text-xs font-semibold">
|
||||
{avatar ? (
|
||||
<Image
|
||||
src={resolveMediaUrl(avatar)}
|
||||
alt={title}
|
||||
width={40}
|
||||
height={40}
|
||||
unoptimized
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
getChatInitials(chat, currentUserId, identity)
|
||||
)}
|
||||
</span>
|
||||
<span className="app-badge app-badge-accent absolute -bottom-0.5 -left-0.5 z-10 flex h-4 w-4 items-center justify-center rounded-full border-2 border-[var(--surface-primary)]">
|
||||
{getChatTypeIcon(chat)}
|
||||
</span>
|
||||
{unread > 0 ? (
|
||||
<span className="chat-list-unread-badge app-counter-danger absolute right-0 top-0 z-20 flex min-w-[1.35rem] -translate-y-1/4 translate-x-1/4 items-center justify-center px-1.5 text-[10px] font-bold leading-none">
|
||||
{unread > 99 ? "99+" : unread}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Plus } from "lucide-react";
|
||||
import { useCallback, useSyncExternalStore } from "react";
|
||||
import { ChevronLeft, MessageSquare, Plus } from "lucide-react";
|
||||
|
||||
import MessagesChatListControls from "@/components/messages/MessagesChatListControls";
|
||||
import MessagesChatListItem from "@/components/messages/MessagesChatListItem";
|
||||
@ -16,6 +17,9 @@ type MessagesFocusSidebarProps = {
|
||||
onCreateChat: () => void;
|
||||
};
|
||||
|
||||
const SIDEBAR_COLLAPSED_STORAGE_PREFIX = "messages.chat-list-collapsed";
|
||||
const SIDEBAR_COLLAPSED_CHANGE_EVENT = "messages-chat-list-collapsed-change";
|
||||
|
||||
export default function MessagesFocusSidebar({
|
||||
activeChatId,
|
||||
mode,
|
||||
@ -23,12 +27,73 @@ export default function MessagesFocusSidebar({
|
||||
onCreateChat,
|
||||
}: MessagesFocusSidebarProps) {
|
||||
const { user, identity, visibleChats, chatTypeCounts, loading, refreshChat } = useMessagesDirectory();
|
||||
const sidebarStorageKey = user?.id
|
||||
? `${SIDEBAR_COLLAPSED_STORAGE_PREFIX}.${user.id}`
|
||||
: null;
|
||||
const subscribeToSidebarState = useCallback((notify: () => void) => {
|
||||
if (!sidebarStorageKey) return () => undefined;
|
||||
const handleStorage = (event: StorageEvent) => {
|
||||
if (event.key === sidebarStorageKey) notify();
|
||||
};
|
||||
window.addEventListener("storage", handleStorage);
|
||||
window.addEventListener(SIDEBAR_COLLAPSED_CHANGE_EVENT, notify);
|
||||
return () => {
|
||||
window.removeEventListener("storage", handleStorage);
|
||||
window.removeEventListener(SIDEBAR_COLLAPSED_CHANGE_EVENT, notify);
|
||||
};
|
||||
}, [sidebarStorageKey]);
|
||||
const getSidebarState = useCallback(
|
||||
() => Boolean(
|
||||
sidebarStorageKey
|
||||
&& window.localStorage.getItem(sidebarStorageKey) === "1"
|
||||
),
|
||||
[sidebarStorageKey],
|
||||
);
|
||||
const sidebarCollapsed = useSyncExternalStore(
|
||||
subscribeToSidebarState,
|
||||
getSidebarState,
|
||||
() => false,
|
||||
);
|
||||
|
||||
const toggleSidebarCollapsed = () => {
|
||||
if (!sidebarStorageKey) return;
|
||||
window.localStorage.setItem(
|
||||
sidebarStorageKey,
|
||||
sidebarCollapsed ? "0" : "1",
|
||||
);
|
||||
window.dispatchEvent(new Event(SIDEBAR_COLLAPSED_CHANGE_EVENT));
|
||||
};
|
||||
|
||||
return (
|
||||
<aside data-testid="messages-focus-sidebar" className="app-surface hidden min-h-0 w-[24rem] shrink-0 flex-col overflow-hidden rounded-2xl lg:flex">
|
||||
<div className="flex items-center justify-between gap-2 p-4 pb-0">
|
||||
<p className="app-card-caption min-w-0 truncate">Сообщения</p>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<aside
|
||||
data-testid="messages-focus-sidebar"
|
||||
className={`hidden min-h-0 shrink-0 flex-col overflow-hidden transition-[width] duration-200 lg:flex ${
|
||||
sidebarCollapsed
|
||||
? "w-[4.75rem] bg-transparent"
|
||||
: "app-surface w-[24rem] rounded-2xl"
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center gap-2 pb-0 ${sidebarCollapsed ? "justify-center p-3" : "justify-between p-4"}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleSidebarCollapsed}
|
||||
className={`app-action-ghost flex items-center justify-center rounded-lg ${
|
||||
sidebarCollapsed ? "h-9 w-9" : "min-w-0 gap-1 px-1 py-1"
|
||||
}`}
|
||||
title={sidebarCollapsed ? "Развернуть список чатов" : "Свернуть список чатов"}
|
||||
aria-label={sidebarCollapsed ? "Развернуть список чатов" : "Свернуть список чатов"}
|
||||
aria-expanded={!sidebarCollapsed}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
<MessageSquare size={17} />
|
||||
) : (
|
||||
<>
|
||||
<ChevronLeft size={14} className="shrink-0" />
|
||||
<span className="app-card-caption min-w-0 truncate">Сообщения</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{!sidebarCollapsed ? <div className="flex shrink-0 items-center gap-1">
|
||||
<MessagesFocusModeButton
|
||||
enabled={mode !== "standard"}
|
||||
onChange={(enabled) => onModeChange(enabled ? "wide" : "standard")}
|
||||
@ -42,14 +107,14 @@ export default function MessagesFocusSidebar({
|
||||
<Plus size={14} />
|
||||
Создать чат
|
||||
</button>
|
||||
</div>
|
||||
</div> : null}
|
||||
</div>
|
||||
|
||||
<div className="p-4 pb-0">
|
||||
{!sidebarCollapsed ? <div className="p-4 pb-0">
|
||||
<MessagesChatListControls userId={user?.id} chatTypeCounts={chatTypeCounts} />
|
||||
</div>
|
||||
</div> : null}
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-2 overflow-y-auto px-4 pb-4">
|
||||
<div className={`min-h-0 flex-1 space-y-2 overflow-y-auto pb-4 ${sidebarCollapsed ? "px-2 pt-2" : "px-4"}`}>
|
||||
{loading ? (
|
||||
<p className="app-text-muted px-3 py-6 text-center text-xs">Загрузка чатов...</p>
|
||||
) : visibleChats.length === 0 ? (
|
||||
@ -61,6 +126,7 @@ export default function MessagesFocusSidebar({
|
||||
currentUserId={user?.id}
|
||||
identity={identity}
|
||||
active={chat.id === activeChatId}
|
||||
compact={sidebarCollapsed}
|
||||
onChatUpdated={refreshChat}
|
||||
/>
|
||||
))}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user