refactor progress
This commit is contained in:
264
packages/schema/src/domains/channel/GuildChannelOrdering.tsx
Normal file
264
packages/schema/src/domains/channel/GuildChannelOrdering.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
|
||||
export interface ChannelOrderingChannel<Id extends string | bigint = string> {
|
||||
id: Id;
|
||||
parentId?: Id | null | undefined;
|
||||
type: number;
|
||||
position?: number | null | undefined;
|
||||
}
|
||||
|
||||
export type GuildChannelReorderErrorCode =
|
||||
| 'TARGET_CHANNEL_NOT_FOUND'
|
||||
| 'PRECEDING_CHANNEL_NOT_FOUND'
|
||||
| 'CANNOT_POSITION_RELATIVE_TO_SELF_BLOCK'
|
||||
| 'PRECEDING_PARENT_MISMATCH'
|
||||
| 'PARENT_NOT_FOUND'
|
||||
| 'PARENT_NOT_CATEGORY'
|
||||
| 'CATEGORIES_CANNOT_HAVE_PARENTS'
|
||||
| 'PARENT_NOT_IN_GUILD_LIST'
|
||||
| 'PRECEDING_NOT_IN_GUILD_LIST';
|
||||
|
||||
export interface GuildChannelReorderOperation<Id extends string | bigint> {
|
||||
channelId: Id;
|
||||
parentId: Id | null | undefined;
|
||||
precedingSiblingId: Id | null | undefined;
|
||||
}
|
||||
|
||||
export interface GuildChannelReorderPlan<Id extends string | bigint, Channel extends ChannelOrderingChannel<Id>> {
|
||||
orderedChannels: Array<Channel>;
|
||||
finalChannels: Array<Channel>;
|
||||
desiredParentById: Map<Id, Id | null>;
|
||||
orderUnchanged: boolean;
|
||||
}
|
||||
|
||||
function idToString<Id extends string | bigint>(id: Id): string {
|
||||
return String(id);
|
||||
}
|
||||
|
||||
export function compareChannelOrdering<Id extends string | bigint>(
|
||||
a: ChannelOrderingChannel<Id>,
|
||||
b: ChannelOrderingChannel<Id>,
|
||||
): number {
|
||||
const aPos = a.position ?? 0;
|
||||
const bPos = b.position ?? 0;
|
||||
if (aPos !== bPos) return aPos - bPos;
|
||||
return idToString(a.id).localeCompare(idToString(b.id));
|
||||
}
|
||||
|
||||
export function sortChannelsForOrdering<Id extends string | bigint, Channel extends ChannelOrderingChannel<Id>>(
|
||||
channels: ReadonlyArray<Channel>,
|
||||
): Array<Channel> {
|
||||
return [...channels].sort(compareChannelOrdering);
|
||||
}
|
||||
|
||||
export function computeChannelMoveBlockIds<Id extends string | bigint, Channel extends ChannelOrderingChannel<Id>>({
|
||||
channels,
|
||||
targetId,
|
||||
}: {
|
||||
channels: ReadonlyArray<Channel>;
|
||||
targetId: Id;
|
||||
}): Set<Id> {
|
||||
const channelById = new Map<Id, Channel>(channels.map((ch) => [ch.id, ch]));
|
||||
const target = channelById.get(targetId);
|
||||
|
||||
const blockIds = new Set<Id>();
|
||||
blockIds.add(targetId);
|
||||
|
||||
if (target?.type === ChannelTypes.GUILD_CATEGORY) {
|
||||
for (const channel of channels) {
|
||||
if (channel.parentId === targetId) {
|
||||
blockIds.add(channel.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blockIds;
|
||||
}
|
||||
|
||||
export function findCategorySpanInOrderedList<Id extends string | bigint, Channel extends ChannelOrderingChannel<Id>>(
|
||||
orderedChannels: ReadonlyArray<Channel>,
|
||||
categoryId: Id,
|
||||
): {start: number; end: number} {
|
||||
const start = orderedChannels.findIndex((ch) => ch.id === categoryId);
|
||||
if (start === -1) return {start: -1, end: -1};
|
||||
let end = start + 1;
|
||||
while (end < orderedChannels.length && orderedChannels[end].parentId === categoryId) {
|
||||
end++;
|
||||
}
|
||||
return {start, end};
|
||||
}
|
||||
|
||||
export function computePrecedingSiblingIdFromPosition<
|
||||
Id extends string | bigint,
|
||||
Channel extends ChannelOrderingChannel<Id>,
|
||||
>({
|
||||
channels,
|
||||
targetId,
|
||||
desiredParentId,
|
||||
position,
|
||||
}: {
|
||||
channels: ReadonlyArray<Channel>;
|
||||
targetId: Id;
|
||||
desiredParentId: Id | null;
|
||||
position: number;
|
||||
}): Id | null {
|
||||
const siblings = sortChannelsForOrdering(channels).filter((ch) => (ch.parentId ?? null) === desiredParentId);
|
||||
const blockIds = computeChannelMoveBlockIds({channels, targetId});
|
||||
const siblingsWithoutBlock = siblings.filter((ch) => !blockIds.has(ch.id));
|
||||
|
||||
const clamped = Math.min(Math.max(position, 0), siblingsWithoutBlock.length);
|
||||
return clamped === 0 ? null : siblingsWithoutBlock[clamped - 1]!.id;
|
||||
}
|
||||
|
||||
export function computePositionFromPrecedingSiblingId<
|
||||
Id extends string | bigint,
|
||||
Channel extends ChannelOrderingChannel<Id>,
|
||||
>({
|
||||
channels,
|
||||
targetId,
|
||||
desiredParentId,
|
||||
precedingSiblingId,
|
||||
}: {
|
||||
channels: ReadonlyArray<Channel>;
|
||||
targetId: Id;
|
||||
desiredParentId: Id | null;
|
||||
precedingSiblingId: Id | null;
|
||||
}): number | null {
|
||||
const siblings = sortChannelsForOrdering(channels).filter((ch) => (ch.parentId ?? null) === desiredParentId);
|
||||
const blockIds = computeChannelMoveBlockIds({channels, targetId});
|
||||
const siblingsWithoutBlock = siblings.filter((ch) => !blockIds.has(ch.id));
|
||||
|
||||
if (!precedingSiblingId) return 0;
|
||||
const index = siblingsWithoutBlock.findIndex((ch) => ch.id === precedingSiblingId);
|
||||
if (index === -1) return null;
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
export function computeGuildChannelReorderPlan<Id extends string | bigint, Channel extends ChannelOrderingChannel<Id>>({
|
||||
channels,
|
||||
operation,
|
||||
}: {
|
||||
channels: ReadonlyArray<Channel>;
|
||||
operation: GuildChannelReorderOperation<Id>;
|
||||
}): {ok: true; plan: GuildChannelReorderPlan<Id, Channel>} | {ok: false; code: GuildChannelReorderErrorCode} {
|
||||
const orderedChannels = sortChannelsForOrdering(channels);
|
||||
const channelById = new Map<Id, Channel>(orderedChannels.map((ch) => [ch.id, ch]));
|
||||
|
||||
const targetChannel = channelById.get(operation.channelId);
|
||||
if (!targetChannel) {
|
||||
return {ok: false, code: 'TARGET_CHANNEL_NOT_FOUND'};
|
||||
}
|
||||
|
||||
const requestedParentId = operation.parentId;
|
||||
const desiredParentId =
|
||||
targetChannel.type === ChannelTypes.GUILD_CATEGORY
|
||||
? null
|
||||
: requestedParentId !== undefined
|
||||
? requestedParentId
|
||||
: (targetChannel.parentId ?? null);
|
||||
|
||||
if (targetChannel.type === ChannelTypes.GUILD_CATEGORY && operation.parentId) {
|
||||
return {ok: false, code: 'CATEGORIES_CANNOT_HAVE_PARENTS'};
|
||||
}
|
||||
|
||||
if (desiredParentId) {
|
||||
const parentChannel = channelById.get(desiredParentId);
|
||||
if (!parentChannel) {
|
||||
return {ok: false, code: 'PARENT_NOT_FOUND'};
|
||||
}
|
||||
if (parentChannel.type !== ChannelTypes.GUILD_CATEGORY) {
|
||||
return {ok: false, code: 'PARENT_NOT_CATEGORY'};
|
||||
}
|
||||
}
|
||||
|
||||
const precedingId = operation.precedingSiblingId ?? null;
|
||||
if (precedingId && !channelById.has(precedingId)) {
|
||||
return {ok: false, code: 'PRECEDING_CHANNEL_NOT_FOUND'};
|
||||
}
|
||||
|
||||
const blockIds = computeChannelMoveBlockIds({channels: orderedChannels, targetId: targetChannel.id});
|
||||
if (precedingId && blockIds.has(precedingId)) {
|
||||
return {ok: false, code: 'CANNOT_POSITION_RELATIVE_TO_SELF_BLOCK'};
|
||||
}
|
||||
|
||||
const remainingChannels = orderedChannels.filter((ch) => !blockIds.has(ch.id));
|
||||
const blockChannels = orderedChannels.filter((ch) => blockIds.has(ch.id));
|
||||
|
||||
const expectedParent = desiredParentId ?? null;
|
||||
if (precedingId) {
|
||||
const precedingChannel = channelById.get(precedingId)!;
|
||||
const precedingParent = precedingChannel.parentId ?? null;
|
||||
if (precedingParent !== expectedParent) {
|
||||
return {ok: false, code: 'PRECEDING_PARENT_MISMATCH'};
|
||||
}
|
||||
}
|
||||
|
||||
let insertIndex = 0;
|
||||
if (precedingId) {
|
||||
const precedingIndex = remainingChannels.findIndex((ch) => ch.id === precedingId);
|
||||
if (precedingIndex === -1) {
|
||||
return {ok: false, code: 'PRECEDING_NOT_IN_GUILD_LIST'};
|
||||
}
|
||||
const precedingChannel = channelById.get(precedingId)!;
|
||||
if (precedingChannel.type === ChannelTypes.GUILD_CATEGORY) {
|
||||
const span = findCategorySpanInOrderedList(remainingChannels, precedingChannel.id);
|
||||
insertIndex = span.end;
|
||||
} else {
|
||||
insertIndex = precedingIndex + 1;
|
||||
}
|
||||
} else if (desiredParentId) {
|
||||
const parentIndex = remainingChannels.findIndex((ch) => ch.id === desiredParentId);
|
||||
if (parentIndex === -1) {
|
||||
return {ok: false, code: 'PARENT_NOT_IN_GUILD_LIST'};
|
||||
}
|
||||
insertIndex = parentIndex + 1;
|
||||
} else {
|
||||
insertIndex = 0;
|
||||
}
|
||||
|
||||
const finalChannels = [...remainingChannels];
|
||||
finalChannels.splice(insertIndex, 0, ...blockChannels);
|
||||
|
||||
const desiredParentById = new Map<Id, Id | null>();
|
||||
for (const channel of finalChannels) {
|
||||
if (channel.id === targetChannel.id) {
|
||||
desiredParentById.set(channel.id, desiredParentId ?? null);
|
||||
} else {
|
||||
desiredParentById.set(channel.id, channel.parentId ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
const orderUnchanged =
|
||||
finalChannels.length === orderedChannels.length &&
|
||||
finalChannels.every((channel, index) => channel.id === orderedChannels[index]!.id) &&
|
||||
(targetChannel.parentId ?? null) === (desiredParentById.get(targetChannel.id) ?? null);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
plan: {
|
||||
orderedChannels,
|
||||
finalChannels,
|
||||
desiredParentById,
|
||||
orderUnchanged,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user