mirror of
https://github.com/docmost/docmost.git
synced 2026-06-10 01:52:43 +08:00
feat(bases): add resolveCardDrop helper for kanban drag mutations
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
vi.mock("fractional-indexing-jittered", () => ({
|
||||
generateJitteredKeyBetween: (a: string | null, b: string | null) =>
|
||||
`${a ?? "START"}|${b ?? "END"}`,
|
||||
}));
|
||||
|
||||
import { resolveCardDrop } from "../resolve-card-drop";
|
||||
import { NO_VALUE_CHOICE_ID } from "@/features/base/types/base.types";
|
||||
|
||||
const mkRow = (id: string, position: string) =>
|
||||
({ id, position, cells: {} }) as any;
|
||||
|
||||
describe("resolveCardDrop", () => {
|
||||
it("returns cells-only when cross-column drop happens with sort active", () => {
|
||||
const result = resolveCardDrop({
|
||||
draggedCardId: "r1",
|
||||
targetCardId: "r2",
|
||||
edge: "top",
|
||||
sourceColumnKey: "c1",
|
||||
targetColumnKey: "c2",
|
||||
groupByPropertyId: "prop-status",
|
||||
columnRows: [mkRow("r2", "b")],
|
||||
sortsActive: true,
|
||||
});
|
||||
expect(result.cells).toEqual({ "prop-status": "c2" });
|
||||
expect(result.position).toBeUndefined();
|
||||
});
|
||||
|
||||
it("writes null cell value when target is the NO_VALUE column", () => {
|
||||
const result = resolveCardDrop({
|
||||
draggedCardId: "r1",
|
||||
targetCardId: "r2",
|
||||
edge: "top",
|
||||
sourceColumnKey: "c1",
|
||||
targetColumnKey: NO_VALUE_CHOICE_ID,
|
||||
groupByPropertyId: "prop-status",
|
||||
columnRows: [mkRow("r2", "b")],
|
||||
sortsActive: false,
|
||||
});
|
||||
expect(result.cells).toEqual({ "prop-status": null });
|
||||
});
|
||||
|
||||
it("returns position-only when intra-column drop with no sort", () => {
|
||||
const result = resolveCardDrop({
|
||||
draggedCardId: "r1",
|
||||
targetCardId: "r2",
|
||||
edge: "bottom",
|
||||
sourceColumnKey: "c1",
|
||||
targetColumnKey: "c1",
|
||||
groupByPropertyId: "prop-status",
|
||||
columnRows: [mkRow("r2", "a"), mkRow("r3", "c")],
|
||||
sortsActive: false,
|
||||
});
|
||||
expect(result.cells).toBeUndefined();
|
||||
expect(typeof result.position).toBe("string");
|
||||
// Between 'a' (r2) and 'c' (r3) → some key, exact value depends on jitter
|
||||
// but must satisfy 'a' < key < 'c' for typical jitter outputs.
|
||||
expect(result.position! > "a").toBe(true);
|
||||
expect(result.position! < "c").toBe(true);
|
||||
});
|
||||
|
||||
it("returns both cells and position for cross-column with slot", () => {
|
||||
const result = resolveCardDrop({
|
||||
draggedCardId: "r1",
|
||||
targetCardId: "r2",
|
||||
edge: "top",
|
||||
sourceColumnKey: "c1",
|
||||
targetColumnKey: "c2",
|
||||
groupByPropertyId: "prop-status",
|
||||
columnRows: [mkRow("r2", "b"), mkRow("r3", "d")],
|
||||
sortsActive: false,
|
||||
});
|
||||
expect(result.cells).toEqual({ "prop-status": "c2" });
|
||||
expect(typeof result.position).toBe("string");
|
||||
expect(result.position! < "b").toBe(true);
|
||||
});
|
||||
|
||||
it("appends to the end when targetCardId is not in columnRows (empty/below-last)", () => {
|
||||
const result = resolveCardDrop({
|
||||
draggedCardId: "r1",
|
||||
targetCardId: "__column-body__", // sentinel
|
||||
edge: "bottom",
|
||||
sourceColumnKey: "c1",
|
||||
targetColumnKey: "c2",
|
||||
groupByPropertyId: "prop-status",
|
||||
columnRows: [mkRow("r5", "z")],
|
||||
sortsActive: false,
|
||||
});
|
||||
expect(result.cells).toEqual({ "prop-status": "c2" });
|
||||
expect(result.position! > "z").toBe(true);
|
||||
});
|
||||
|
||||
it("returns undefined for both fields when same column and sort active", () => {
|
||||
const result = resolveCardDrop({
|
||||
draggedCardId: "r1",
|
||||
targetCardId: "r2",
|
||||
edge: "top",
|
||||
sourceColumnKey: "c1",
|
||||
targetColumnKey: "c1",
|
||||
groupByPropertyId: "prop-status",
|
||||
columnRows: [mkRow("r2", "a")],
|
||||
sortsActive: true,
|
||||
});
|
||||
expect(result.cells).toBeUndefined();
|
||||
expect(result.position).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { generateJitteredKeyBetween } from "fractional-indexing-jittered";
|
||||
import { IBaseRow, NO_VALUE_CHOICE_ID } from "@/features/base/types/base.types";
|
||||
|
||||
type Edge = "top" | "bottom";
|
||||
|
||||
export type ResolveCardDropInput = {
|
||||
draggedCardId: string;
|
||||
targetCardId: string;
|
||||
edge: Edge | null;
|
||||
sourceColumnKey: string;
|
||||
targetColumnKey: string;
|
||||
groupByPropertyId: string;
|
||||
columnRows: IBaseRow[]; // rows currently in the target column, in display order
|
||||
sortsActive: boolean;
|
||||
};
|
||||
|
||||
export type ResolveCardDropResult = {
|
||||
cells: Record<string, unknown> | undefined;
|
||||
position: string | undefined;
|
||||
};
|
||||
|
||||
export function resolveCardDrop(
|
||||
input: ResolveCardDropInput,
|
||||
): ResolveCardDropResult {
|
||||
const {
|
||||
draggedCardId,
|
||||
targetCardId,
|
||||
edge,
|
||||
sourceColumnKey,
|
||||
targetColumnKey,
|
||||
groupByPropertyId,
|
||||
columnRows,
|
||||
sortsActive,
|
||||
} = input;
|
||||
|
||||
const sameColumn = sourceColumnKey === targetColumnKey;
|
||||
|
||||
// Compute the cells patch first.
|
||||
const cells = sameColumn
|
||||
? undefined
|
||||
: {
|
||||
[groupByPropertyId]:
|
||||
targetColumnKey === NO_VALUE_CHOICE_ID ? null : targetColumnKey,
|
||||
};
|
||||
|
||||
// Same column + sort active → block (caller should have stopped the drag
|
||||
// via canDrop, but we return no-op for safety).
|
||||
if (sameColumn && sortsActive) {
|
||||
return { cells: undefined, position: undefined };
|
||||
}
|
||||
|
||||
// Sort active and cross-column → only cells, no position.
|
||||
if (sortsActive) {
|
||||
return { cells, position: undefined };
|
||||
}
|
||||
|
||||
// Sort inactive → compute the slot.
|
||||
// Filter the dragged card out of the column (it may already be there
|
||||
// when intra-column drag).
|
||||
const target = columnRows.filter((r) => r.id !== draggedCardId);
|
||||
const targetIndex = target.findIndex((r) => r.id === targetCardId);
|
||||
|
||||
let lower: string | null;
|
||||
let upper: string | null;
|
||||
|
||||
if (targetIndex === -1) {
|
||||
// Drop on column body / sentinel / below-last → append.
|
||||
const last = target[target.length - 1];
|
||||
lower = last?.position ?? null;
|
||||
upper = null;
|
||||
} else if (edge === "top") {
|
||||
lower = target[targetIndex - 1]?.position ?? null;
|
||||
upper = target[targetIndex].position;
|
||||
} else {
|
||||
lower = target[targetIndex].position;
|
||||
upper = target[targetIndex + 1]?.position ?? null;
|
||||
}
|
||||
|
||||
let position: string;
|
||||
try {
|
||||
position = generateJitteredKeyBetween(lower, upper);
|
||||
} catch {
|
||||
// Identical keys (rare; happens when two rows briefly share a position
|
||||
// during a concurrent edit). Fall back to insert-after-lower.
|
||||
position = generateJitteredKeyBetween(lower, null);
|
||||
}
|
||||
return { cells, position };
|
||||
}
|
||||
Reference in New Issue
Block a user