mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-25 03:55:27 +02:00
- sort: direction now applies to value comparison only; issues without a value sort last in BOTH directions (the whole-array reverse flipped them to the front on desc). Test covers the desc+missing case. - board: values referencing an option removed from the definition bucket into the No-value column instead of vanishing (unmatched column ids dropped the issue entirely). Defense-in-depth behind the new server-side in-use guard; drag-utils test locks both behaviors. - controller: persisted propertyFilters keyed by archived/deleted definitions are stripped before reaching the filter predicates, and a persisted property sort on a non-active definition degrades to manual order — previously both kept silently applying while the header claimed otherwise. The filter badge counts only active-definition filters. Co-authored-by: multica-agent <github@multica.ai>
113 lines
4.0 KiB
TypeScript
113 lines
4.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { Issue } from "@multica/core/types";
|
|
import { getIssueGroupId, getMoveUpdates, insertIdByPosition, issueMatchesGroup, propertyGroupId } from "./drag-utils";
|
|
|
|
function mk(id: string, position: number): Issue {
|
|
return {
|
|
id,
|
|
workspace_id: "ws-1",
|
|
number: 1,
|
|
identifier: `MUL-${id}`,
|
|
title: id,
|
|
description: null,
|
|
status: "todo",
|
|
priority: "none",
|
|
assignee_type: null,
|
|
assignee_id: null,
|
|
creator_type: "member",
|
|
creator_id: "user-1",
|
|
parent_issue_id: null,
|
|
project_id: null,
|
|
position,
|
|
stage: null,
|
|
start_date: null,
|
|
due_date: null,
|
|
metadata: {},
|
|
properties: {},
|
|
labels: [],
|
|
created_at: "2025-01-01T00:00:00Z",
|
|
updated_at: "2025-01-01T00:00:00Z",
|
|
};
|
|
}
|
|
|
|
function mapOf(...issues: Issue[]): Map<string, Issue> {
|
|
return new Map(issues.map((i) => [i.id, i]));
|
|
}
|
|
|
|
describe("insertIdByPosition", () => {
|
|
it("inserts the id at its position-sorted slot", () => {
|
|
const map = mapOf(mk("a", 1), mk("c", 3), mk("b", 2));
|
|
expect(insertIdByPosition(["a", "c"], "b", 2, map)).toEqual([
|
|
"a",
|
|
"b",
|
|
"c",
|
|
]);
|
|
});
|
|
|
|
it("appends when the position is the largest", () => {
|
|
const map = mapOf(mk("a", 1), mk("z", 9));
|
|
expect(insertIdByPosition(["a"], "z", 9, map)).toEqual(["a", "z"]);
|
|
});
|
|
|
|
it("prepends when the position is the smallest", () => {
|
|
const map = mapOf(mk("b", 2), mk("a", 1));
|
|
expect(insertIdByPosition(["b"], "a", 1, map)).toEqual(["a", "b"]);
|
|
});
|
|
|
|
it("appends into an empty target column", () => {
|
|
const map = mapOf(mk("a", 5));
|
|
expect(insertIdByPosition([], "a", 5, map)).toEqual(["a"]);
|
|
});
|
|
|
|
it("matches insertByPosition ordering so the settle rebuild is a no-op", () => {
|
|
// Same scenario the board's optimistic drop and the cache patch both apply:
|
|
// landing a card between two neighbours must produce the same order in the
|
|
// id list (board) and the issue list (cache).
|
|
const map = mapOf(mk("x", 1), mk("y", 3), mk("moved", 2));
|
|
expect(insertIdByPosition(["x", "y"], "moved", 2, map)).toEqual([
|
|
"x",
|
|
"moved",
|
|
"y",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("property grouping", () => {
|
|
const propertyId = "prop-env";
|
|
const withValue = { id: "A", properties: { [propertyId]: "opt-staging" } } as unknown as Issue;
|
|
const withoutValue = { id: "B", properties: {} } as unknown as Issue;
|
|
|
|
it("getIssueGroupId buckets by option id, no-value issues into the none column", () => {
|
|
expect(getIssueGroupId(withValue, `property:${propertyId}`)).toBe(
|
|
propertyGroupId(propertyId, "opt-staging"),
|
|
);
|
|
expect(getIssueGroupId(withoutValue, `property:${propertyId}`)).toBe(
|
|
propertyGroupId(propertyId, null),
|
|
);
|
|
});
|
|
|
|
it("issueMatchesGroup distinguishes option and no-value columns", () => {
|
|
const optionColumn = { id: "c1", title: "Staging", propertyId, propertyOptionId: "opt-staging" };
|
|
const noneColumn = { id: "c2", title: "No value", propertyId, propertyOptionId: null };
|
|
expect(issueMatchesGroup(withValue, optionColumn)).toBe(true);
|
|
expect(issueMatchesGroup(withValue, noneColumn)).toBe(false);
|
|
expect(issueMatchesGroup(withoutValue, noneColumn)).toBe(true);
|
|
});
|
|
|
|
it("unknown option values bucket into the none column when the catalog is known", () => {
|
|
const stale = { id: "C", properties: { [propertyId]: "opt-deleted" } } as unknown as Issue;
|
|
const known = new Set(["opt-staging"]);
|
|
expect(getIssueGroupId(stale, `property:${propertyId}`, known)).toBe(
|
|
propertyGroupId(propertyId, null),
|
|
);
|
|
// Without the catalog, the raw bucket is preserved (caller may still map it).
|
|
expect(getIssueGroupId(stale, `property:${propertyId}`)).toBe(
|
|
propertyGroupId(propertyId, "opt-deleted"),
|
|
);
|
|
});
|
|
|
|
it("getMoveUpdates for property columns only carries position", () => {
|
|
expect(getMoveUpdates({ id: "c1", title: "Staging", propertyId, propertyOptionId: "opt-staging" }, 5)).toEqual({ position: 5 });
|
|
});
|
|
});
|