mirror of
https://github.com/dataelement/bisheng.git
synced 2026-09-24 23:19:52 +08:00
feat(knowledge): 知识空间置顶按最近置顶时间排序
后端 list_for_user 返回有序置顶 ID,列表按置顶先后排列;前端抽出 spaceSort,乐观更新时新置顶移到置顶区最前,并覆盖门户侧栏排序。
This commit is contained in:
@@ -288,3 +288,8 @@ sg_test.sh
|
||||
sg-sync-api-preview.md
|
||||
sg-sync-api-内部.md
|
||||
mdf_file_read_count.sh
|
||||
.dev-stack/
|
||||
|
||||
# Local-only 门户联调 (do not commit)
|
||||
.cursor/rules/local-portal-dev-stack.mdc
|
||||
scripts/dev-stack.sh
|
||||
|
||||
+9
-3
@@ -19,14 +19,20 @@ class KnowledgeSpacePinRepositoryImpl(BaseRepositoryImpl[UserLink, int], Knowled
|
||||
async def lock_user(self, user_id: int) -> None:
|
||||
await self.session.exec(select(_USER_TABLE.c.user_id).where(_USER_TABLE.c.user_id == user_id).with_for_update())
|
||||
|
||||
async def list_for_user(self, user_id: int) -> set[int]:
|
||||
async def list_for_user(self, user_id: int) -> list[int]:
|
||||
result = await self.session.exec(
|
||||
select(UserLink.type_detail).where(
|
||||
select(UserLink.type_detail)
|
||||
.where(
|
||||
UserLink.user_id == user_id,
|
||||
UserLink.type == KNOWLEDGE_SPACE_PIN_TYPE,
|
||||
)
|
||||
.order_by(UserLink.create_time.desc(), UserLink.id.desc())
|
||||
)
|
||||
return {int(type_detail) for type_detail in result.all() if str(type_detail).isdigit()}
|
||||
ordered: list[int] = []
|
||||
for type_detail in result.all():
|
||||
if str(type_detail).isdigit():
|
||||
ordered.append(int(type_detail))
|
||||
return ordered
|
||||
|
||||
async def add_pin(self, user_id: int, space_id: int) -> bool:
|
||||
existing = await self.session.exec(
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@ class KnowledgeSpacePinRepository(BaseRepository[Any, int], ABC):
|
||||
async def lock_user(self, user_id: int) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def list_for_user(self, user_id: int) -> set[int]: ...
|
||||
async def list_for_user(self, user_id: int) -> list[int]:
|
||||
"""Return pinned space ids ordered by create_time DESC (most recent first)."""
|
||||
|
||||
@abstractmethod
|
||||
async def add_pin(self, user_id: int, space_id: int) -> bool: ...
|
||||
|
||||
@@ -13,12 +13,17 @@ MAX_PINS_PER_LEVEL = 5
|
||||
|
||||
class KnowledgeSpacePinService:
|
||||
@staticmethod
|
||||
async def get_pinned_space_ids(user_id: int, visible_space_ids: set[int]) -> set[int]:
|
||||
async def list_pinned_space_ids(user_id: int, visible_space_ids: set[int]) -> list[int]:
|
||||
"""Pinned space ids visible to the user, most recently pinned first."""
|
||||
if not visible_space_ids:
|
||||
return set()
|
||||
return []
|
||||
async with get_async_db_session() as session:
|
||||
pinned_ids = await KnowledgeSpacePinRepositoryImpl(session).list_for_user(user_id)
|
||||
return pinned_ids & visible_space_ids
|
||||
return [space_id for space_id in pinned_ids if space_id in visible_space_ids]
|
||||
|
||||
@staticmethod
|
||||
async def get_pinned_space_ids(user_id: int, visible_space_ids: set[int]) -> set[int]:
|
||||
return set(await KnowledgeSpacePinService.list_pinned_space_ids(user_id, visible_space_ids))
|
||||
|
||||
@classmethod
|
||||
async def set_pin(
|
||||
@@ -33,7 +38,7 @@ class KnowledgeSpacePinService:
|
||||
repository = KnowledgeSpacePinRepositoryImpl(session)
|
||||
await repository.lock_user(user_id)
|
||||
pinned_ids = await repository.list_for_user(user_id)
|
||||
current_ids = pinned_ids & visible_space_ids
|
||||
current_ids = [item for item in pinned_ids if item in visible_space_ids]
|
||||
if not is_pinned:
|
||||
await repository.remove_pin(user_id, space_id)
|
||||
await session.commit()
|
||||
@@ -55,19 +60,21 @@ class KnowledgeSpacePinService:
|
||||
for item in spaces
|
||||
if (item.get("id") if isinstance(item, dict) else getattr(item, "id", None)) is not None
|
||||
}
|
||||
pinned_ids = await cls.get_pinned_space_ids(user_id, visible_ids)
|
||||
pinned_order = await cls.list_pinned_space_ids(user_id, visible_ids)
|
||||
pin_rank = {space_id: index for index, space_id in enumerate(pinned_order)}
|
||||
pinned: list[Any] = []
|
||||
normal: list[Any] = []
|
||||
for item in spaces:
|
||||
item_id = int(item.get("id") if isinstance(item, dict) else item.id)
|
||||
level = item.get("space_level") if isinstance(item, dict) else getattr(item, "space_level", None)
|
||||
level_value = getattr(level, "value", level)
|
||||
is_pinned = level_value != KnowledgeSpaceLevelEnum.PERSONAL.value and item_id in pinned_ids
|
||||
is_pinned = level_value != KnowledgeSpaceLevelEnum.PERSONAL.value and item_id in pin_rank
|
||||
if isinstance(item, dict):
|
||||
item["is_pinned"] = is_pinned
|
||||
else:
|
||||
item.is_pinned = is_pinned
|
||||
(pinned if is_pinned else normal).append(item)
|
||||
pinned.sort(key=lambda item: pin_rank[int(item.get("id") if isinstance(item, dict) else item.id)])
|
||||
return pinned + normal
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -42,14 +42,14 @@ async def test_stale_cached_pin_is_reset_before_return():
|
||||
),
|
||||
patch.object(
|
||||
KnowledgeSpacePinService,
|
||||
"get_pinned_space_ids",
|
||||
new=AsyncMock(return_value=set()),
|
||||
) as get_pins,
|
||||
"list_pinned_space_ids",
|
||||
new=AsyncMock(return_value=[]),
|
||||
) as list_pins,
|
||||
):
|
||||
result = await service._list_accessible_spaces()
|
||||
|
||||
assert result[0].is_pinned is False
|
||||
get_pins.assert_awaited_once_with(7, {10})
|
||||
list_pins.assert_awaited_once_with(7, {10})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -63,8 +63,8 @@ async def test_personal_residual_link_never_marks_list_item_pinned():
|
||||
]
|
||||
with patch.object(
|
||||
KnowledgeSpacePinService,
|
||||
"get_pinned_space_ids",
|
||||
new=AsyncMock(return_value={10}),
|
||||
"list_pinned_space_ids",
|
||||
new=AsyncMock(return_value=[10]),
|
||||
):
|
||||
result = await KnowledgeSpacePinService.apply_pins(spaces, user_id=7)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
@@ -34,11 +35,42 @@ async def test_list_add_and_remove_are_scoped_and_idempotent(session: AsyncSessi
|
||||
|
||||
assert await repository.add_pin(1, 10) is True
|
||||
assert await repository.add_pin(1, 10) is False
|
||||
assert await repository.list_for_user(1) == {10}
|
||||
assert await repository.list_for_user(1) == [10]
|
||||
|
||||
assert await repository.remove_pin(1, 999) is False
|
||||
assert await repository.remove_pin(1, 10) is True
|
||||
assert await repository.list_for_user(1) == set()
|
||||
assert await repository.list_for_user(1) == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_for_user_orders_by_create_time_desc(session: AsyncSession):
|
||||
repository = KnowledgeSpacePinRepositoryImpl(session)
|
||||
now = datetime.now()
|
||||
session.add_all(
|
||||
[
|
||||
UserLink(
|
||||
user_id=1,
|
||||
type="knowledge_space_pin",
|
||||
type_detail="10",
|
||||
create_time=now - timedelta(minutes=2),
|
||||
),
|
||||
UserLink(
|
||||
user_id=1,
|
||||
type="knowledge_space_pin",
|
||||
type_detail="11",
|
||||
create_time=now - timedelta(minutes=1),
|
||||
),
|
||||
UserLink(
|
||||
user_id=1,
|
||||
type="knowledge_space_pin",
|
||||
type_detail="12",
|
||||
create_time=now,
|
||||
),
|
||||
]
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
assert await repository.list_for_user(1) == [12, 11, 10]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -10,7 +10,8 @@ from bisheng.knowledge.domain.services.knowledge_space_pin_service import Knowle
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_pins_intersects_visible_ids_forces_personal_false_and_keeps_stable_order():
|
||||
async def test_apply_pins_orders_by_pin_recency_and_forces_personal_false():
|
||||
# Input list order is 1,2,3,4 but pin order is newest-first: 4 then 2.
|
||||
spaces = [
|
||||
SimpleNamespace(id=1, space_level=KnowledgeSpaceLevelEnum.PUBLIC, is_pinned=False),
|
||||
SimpleNamespace(id=2, space_level=KnowledgeSpaceLevelEnum.PUBLIC, is_pinned=False),
|
||||
@@ -19,13 +20,13 @@ async def test_apply_pins_intersects_visible_ids_forces_personal_false_and_keeps
|
||||
]
|
||||
with patch.object(
|
||||
KnowledgeSpacePinService,
|
||||
"get_pinned_space_ids",
|
||||
new=AsyncMock(return_value={2, 3, 4, 999}),
|
||||
) as get_pins:
|
||||
"list_pinned_space_ids",
|
||||
new=AsyncMock(return_value=[4, 2, 3, 999]),
|
||||
) as list_pins:
|
||||
result = await KnowledgeSpacePinService.apply_pins(spaces, user_id=7)
|
||||
|
||||
get_pins.assert_awaited_once_with(7, {1, 2, 3, 4})
|
||||
assert [item.id for item in result] == [2, 4, 1, 3]
|
||||
list_pins.assert_awaited_once_with(7, {1, 2, 3, 4})
|
||||
assert [item.id for item in result] == [4, 2, 1, 3]
|
||||
assert [item.is_pinned for item in result] == [True, True, False, False]
|
||||
|
||||
|
||||
@@ -37,8 +38,8 @@ async def test_apply_pins_supports_public_list_dicts():
|
||||
]
|
||||
with patch.object(
|
||||
KnowledgeSpacePinService,
|
||||
"get_pinned_space_ids",
|
||||
new=AsyncMock(return_value={2}),
|
||||
"list_pinned_space_ids",
|
||||
new=AsyncMock(return_value=[2]),
|
||||
):
|
||||
result = await KnowledgeSpacePinService.apply_pins(spaces, user_id=7)
|
||||
|
||||
@@ -46,11 +47,35 @@ async def test_apply_pins_supports_public_list_dicts():
|
||||
assert [item["is_pinned"] for item in result] == [True, False]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_pins_re_pin_moves_space_to_front():
|
||||
"""Pin A then B → [B, A]; unpin A and re-pin A → [A, B]."""
|
||||
spaces = [
|
||||
SimpleNamespace(id=1, space_level=KnowledgeSpaceLevelEnum.PUBLIC, is_pinned=False),
|
||||
SimpleNamespace(id=2, space_level=KnowledgeSpaceLevelEnum.PUBLIC, is_pinned=False),
|
||||
]
|
||||
with patch.object(
|
||||
KnowledgeSpacePinService,
|
||||
"list_pinned_space_ids",
|
||||
new=AsyncMock(return_value=[2, 1]),
|
||||
):
|
||||
after_pin_b = await KnowledgeSpacePinService.apply_pins(spaces, user_id=7)
|
||||
assert [item.id for item in after_pin_b] == [2, 1]
|
||||
|
||||
with patch.object(
|
||||
KnowledgeSpacePinService,
|
||||
"list_pinned_space_ids",
|
||||
new=AsyncMock(return_value=[1, 2]),
|
||||
):
|
||||
after_re_pin_a = await KnowledgeSpacePinService.apply_pins(spaces, user_id=7)
|
||||
assert [item.id for item in after_re_pin_a] == [1, 2]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_pin_rejects_sixth_visible_pin_without_writing():
|
||||
repository = SimpleNamespace(
|
||||
lock_user=AsyncMock(),
|
||||
list_for_user=AsyncMock(return_value=set(range(1, 6))),
|
||||
list_for_user=AsyncMock(return_value=list(range(1, 6))),
|
||||
add_pin=AsyncMock(),
|
||||
remove_pin=AsyncMock(),
|
||||
)
|
||||
@@ -87,7 +112,7 @@ async def test_set_pin_rejects_sixth_visible_pin_without_writing():
|
||||
async def test_set_pin_is_idempotent_for_existing_pin_even_at_limit():
|
||||
repository = SimpleNamespace(
|
||||
lock_user=AsyncMock(),
|
||||
list_for_user=AsyncMock(return_value=set(range(1, 6))),
|
||||
list_for_user=AsyncMock(return_value=list(range(1, 6))),
|
||||
add_pin=AsyncMock(),
|
||||
remove_pin=AsyncMock(),
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { NotificationSeverity } from "~/common";
|
||||
import { useToastContext } from "~/Providers";
|
||||
import { useLocalize } from "~/hooks";
|
||||
import { extractKnowledgeActionErrorMessage } from "../errorUtils";
|
||||
import { applyPinOrderToSpaceList } from "../sidebar/spaceSort";
|
||||
|
||||
interface UseSpaceActionsOptions {
|
||||
activeSpaceId?: string;
|
||||
@@ -142,9 +143,10 @@ export function useSpaceActions({
|
||||
return;
|
||||
}
|
||||
|
||||
// Optimistic update
|
||||
const updater = (list: KnowledgeSpace[]) => list.map(s => s.id === spaceId ? { ...s, isPinned: pinned } : s);
|
||||
updateAllCaches(updater);
|
||||
const previousGrouped = queryClient.getQueryData<GroupedKnowledgeSpaces>(["knowledgeSpaces", "grouped"]);
|
||||
|
||||
// Optimistic update: newly pinned spaces move to the front of the pinned block.
|
||||
updateAllCaches((list) => applyPinOrderToSpaceList(list, spaceId, pinned));
|
||||
|
||||
if (activeSpaceId === spaceId) {
|
||||
const space = targetList.find(s => s.id === spaceId);
|
||||
@@ -156,9 +158,11 @@ export function useSpaceActions({
|
||||
queryClient.invalidateQueries({ queryKey: ["knowledgeSpaces"] });
|
||||
showToast({ message: pinned ? localize("com_knowledge.pinned") : localize("com_knowledge.unpinned"), severity: NotificationSeverity.SUCCESS });
|
||||
} catch {
|
||||
// Rollback
|
||||
const rollback = (list: KnowledgeSpace[]) => list.map(s => s.id === spaceId ? { ...s, isPinned: !pinned } : s);
|
||||
updateAllCaches(rollback);
|
||||
if (previousGrouped) {
|
||||
queryClient.setQueryData(["knowledgeSpaces", "grouped"], previousGrouped);
|
||||
} else {
|
||||
updateAllCaches((list) => applyPinOrderToSpaceList(list, spaceId, !pinned));
|
||||
}
|
||||
if (activeSpaceId === spaceId) {
|
||||
const space = targetList.find(s => s.id === spaceId);
|
||||
if (space) onSpaceSelect({ ...space, isPinned: !pinned });
|
||||
|
||||
@@ -463,12 +463,35 @@ export default function PortalKnowledgeWorkbench() {
|
||||
showToast({ message: "最多置顶 5 个知识库", severity: NotificationSeverity.INFO });
|
||||
return;
|
||||
}
|
||||
const queryKey = ["knowledgeSpaces", "level", group.level];
|
||||
const previousSpaces = queryClient.getQueryData<KnowledgeSpace[]>(queryKey);
|
||||
if (previousSpaces) {
|
||||
const target = previousSpaces.find((item) => item.id === space.id);
|
||||
if (target) {
|
||||
const updated = { ...target, isPinned: pinned };
|
||||
const others = previousSpaces.filter((item) => item.id !== space.id);
|
||||
queryClient.setQueryData<KnowledgeSpace[]>(
|
||||
queryKey,
|
||||
pinned
|
||||
? [updated, ...others]
|
||||
: [
|
||||
...others.filter((item) => item.isPinned),
|
||||
updated,
|
||||
...others.filter((item) => !item.isPinned),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
setActiveSpace((prev) => prev?.id === space.id ? { ...prev, isPinned: pinned } : prev);
|
||||
try {
|
||||
await pinSpaceApi(space.id, pinned);
|
||||
setActiveSpace((prev) => prev?.id === space.id ? { ...prev, isPinned: pinned } : prev);
|
||||
await queryClient.invalidateQueries({ queryKey: ["knowledgeSpaces"] });
|
||||
showToast({ message: pinned ? "已置顶" : "已取消置顶", severity: NotificationSeverity.SUCCESS });
|
||||
} catch {
|
||||
if (previousSpaces) {
|
||||
queryClient.setQueryData(queryKey, previousSpaces);
|
||||
}
|
||||
setActiveSpace((prev) => prev?.id === space.id ? { ...prev, isPinned: !pinned } : prev);
|
||||
showToast({ message: "操作失败", severity: NotificationSeverity.ERROR });
|
||||
}
|
||||
}, [queryClient, showToast]);
|
||||
|
||||
@@ -17,6 +17,9 @@ import {
|
||||
hasKnowledgeSpacePermission,
|
||||
useKnowledgeSpaceActionPermissions,
|
||||
} from "../hooks/useKnowledgeSpacePermissions";
|
||||
import { sortKnowledgeSpacesForSection } from "./spaceSort";
|
||||
|
||||
export { applyPinOrderToSpaceList, sortKnowledgeSpacesForSection } from "./spaceSort";
|
||||
|
||||
interface KnowledgeSpaceSidebarProps {
|
||||
activeSpaceId?: string;
|
||||
@@ -64,23 +67,6 @@ function getSortLabel(sort: SpaceSortType, localize: any) {
|
||||
return sort === SpaceSortType.NAME ? localize("com_knowledge.name") : localize("com_knowledge.recently_updated");
|
||||
}
|
||||
|
||||
export function sortKnowledgeSpacesForSection(
|
||||
spaces: KnowledgeSpace[],
|
||||
sortBy: SpaceSortType,
|
||||
): KnowledgeSpace[] {
|
||||
return [...spaces].sort((a, b) => {
|
||||
if (a.isPinned !== b.isPinned) {
|
||||
return a.isPinned ? -1 : 1;
|
||||
}
|
||||
if (sortBy === SpaceSortType.NAME) {
|
||||
return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" });
|
||||
}
|
||||
const timeA = Date.parse(a.updatedAt || "") || 0;
|
||||
const timeB = Date.parse(b.updatedAt || "") || 0;
|
||||
return timeB - timeA;
|
||||
});
|
||||
}
|
||||
|
||||
export function getNextSectionSorts(
|
||||
currentSorts: Record<SpaceLevel, SpaceSortType>,
|
||||
level: SpaceLevel,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { KnowledgeSpace, SpaceLevel, SpaceSortType } from "~/api/knowledge";
|
||||
import { applyPinOrderToSpaceList, sortKnowledgeSpacesForSection } from "./spaceSort";
|
||||
|
||||
function space(
|
||||
id: string,
|
||||
overrides: Partial<KnowledgeSpace> = {},
|
||||
): KnowledgeSpace {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
description: "",
|
||||
icon: "",
|
||||
visibility: "private" as KnowledgeSpace["visibility"],
|
||||
isReleased: true,
|
||||
isPinned: false,
|
||||
spaceLevel: SpaceLevel.PUBLIC,
|
||||
updatedAt: "2026-01-01T00:00:00Z",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("sortKnowledgeSpacesForSection", () => {
|
||||
it("keeps relative order among pinned spaces", () => {
|
||||
const input = [
|
||||
space("b", { isPinned: true, name: "B", updatedAt: "2026-01-02T00:00:00Z" }),
|
||||
space("a", { isPinned: true, name: "A", updatedAt: "2026-01-03T00:00:00Z" }),
|
||||
space("c", { isPinned: false, name: "C" }),
|
||||
];
|
||||
|
||||
const byName = sortKnowledgeSpacesForSection(input, SpaceSortType.NAME);
|
||||
expect(byName.map((item) => item.id)).toEqual(["b", "a", "c"]);
|
||||
|
||||
const byTime = sortKnowledgeSpacesForSection(input, SpaceSortType.UPDATE_TIME);
|
||||
expect(byTime.map((item) => item.id)).toEqual(["b", "a", "c"]);
|
||||
});
|
||||
|
||||
it("still floats pinned spaces above unpinned ones", () => {
|
||||
const input = [
|
||||
space("c", { isPinned: false, name: "C" }),
|
||||
space("a", { isPinned: true, name: "A" }),
|
||||
space("b", { isPinned: false, name: "B" }),
|
||||
];
|
||||
|
||||
expect(
|
||||
sortKnowledgeSpacesForSection(input, SpaceSortType.NAME).map((item) => item.id),
|
||||
).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyPinOrderToSpaceList", () => {
|
||||
it("moves a newly pinned space to the front", () => {
|
||||
const input = [
|
||||
space("a", { isPinned: true }),
|
||||
space("b", { isPinned: false }),
|
||||
space("c", { isPinned: false }),
|
||||
];
|
||||
|
||||
expect(applyPinOrderToSpaceList(input, "c", true).map((item) => item.id)).toEqual([
|
||||
"c",
|
||||
"a",
|
||||
"b",
|
||||
]);
|
||||
expect(applyPinOrderToSpaceList(input, "c", true)[0].isPinned).toBe(true);
|
||||
});
|
||||
|
||||
it("places an unpinned space after the remaining pinned block", () => {
|
||||
const input = [
|
||||
space("b", { isPinned: true }),
|
||||
space("a", { isPinned: true }),
|
||||
space("c", { isPinned: false }),
|
||||
];
|
||||
|
||||
expect(applyPinOrderToSpaceList(input, "a", false).map((item) => item.id)).toEqual([
|
||||
"b",
|
||||
"a",
|
||||
"c",
|
||||
]);
|
||||
expect(applyPinOrderToSpaceList(input, "a", false).find((item) => item.id === "a")?.isPinned).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { KnowledgeSpace, SpaceSortType } from "~/api/knowledge";
|
||||
|
||||
export function sortKnowledgeSpacesForSection(
|
||||
spaces: KnowledgeSpace[],
|
||||
sortBy: SpaceSortType,
|
||||
): KnowledgeSpace[] {
|
||||
return [...spaces].sort((a, b) => {
|
||||
if (a.isPinned !== b.isPinned) {
|
||||
return a.isPinned ? -1 : 1;
|
||||
}
|
||||
// Preserve API / optimistic pin-recency order within the pinned block.
|
||||
if (a.isPinned && b.isPinned) {
|
||||
return 0;
|
||||
}
|
||||
if (sortBy === SpaceSortType.NAME) {
|
||||
return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" });
|
||||
}
|
||||
const timeA = Date.parse(a.updatedAt || "") || 0;
|
||||
const timeB = Date.parse(b.updatedAt || "") || 0;
|
||||
return timeB - timeA;
|
||||
});
|
||||
}
|
||||
|
||||
/** Move a space to the front of its list when pinning; keep relative order otherwise. */
|
||||
export function applyPinOrderToSpaceList(
|
||||
list: KnowledgeSpace[],
|
||||
spaceId: string,
|
||||
pinned: boolean,
|
||||
): KnowledgeSpace[] {
|
||||
const target = list.find((space) => space.id === spaceId);
|
||||
if (!target) {
|
||||
return list;
|
||||
}
|
||||
const updated = { ...target, isPinned: pinned };
|
||||
const others = list.filter((space) => space.id !== spaceId);
|
||||
if (pinned) {
|
||||
return [updated, ...others];
|
||||
}
|
||||
const pinnedItems = others.filter((space) => space.isPinned);
|
||||
const unpinnedItems = others.filter((space) => !space.isPinned);
|
||||
return [...pinnedItems, updated, ...unpinnedItems];
|
||||
}
|
||||
Reference in New Issue
Block a user