mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-17 15:19:00 +02:00
- Settings → Properties: definition management mirroring the Labels tab
(list with type badges/option chips/usage counts, create/edit dialog
with option editor, archive/restore, 20-cap indicator). Admin-gated;
members see a read-only catalog.
- Issue detail sidebar: custom properties join the built-in optional
props' progressive disclosure — set values render as rows with
type-appropriate editors (select/multi-select pickers, calendar,
yes/no, inline input for text/number/url), unset ones live in the
same '+ Add property' menu behind a separator. Archived definitions
render read-only until cleared.
- Core: property types, zod schemas (lenient type strings for forward
compat), api client methods, React Query hooks with optimistic
single-key value writes, ws-updaters + realtime wiring for
property:created/updated and issue_properties:changed.
- Locales: en/zh-Hans/ja/ko strings; Issue fixtures gain properties: {}.
Co-authored-by: multica-agent <github@multica.ai>
118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import type { Issue } from "../types";
|
|
import { commonIssueFields } from "./batch";
|
|
|
|
function makeIssue(overrides: Partial<Issue> = {}): Issue {
|
|
return {
|
|
id: "issue-1",
|
|
workspace_id: "ws-1",
|
|
number: 1,
|
|
identifier: "MUL-1",
|
|
title: "Issue 1",
|
|
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: 1,
|
|
stage: null,
|
|
start_date: null,
|
|
due_date: null,
|
|
metadata: {},
|
|
properties: {},
|
|
created_at: "2026-01-01T00:00:00Z",
|
|
updated_at: "2026-01-01T00:00:00Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("commonIssueFields", () => {
|
|
it("returns all-null for an empty selection (nothing to reflect)", () => {
|
|
expect(commonIssueFields([])).toEqual({
|
|
status: null,
|
|
priority: null,
|
|
assignee: null,
|
|
});
|
|
});
|
|
|
|
it("reflects a single issue's own fields", () => {
|
|
const common = commonIssueFields([
|
|
makeIssue({ status: "in_progress", priority: "high", assignee_type: "member", assignee_id: "u-1" }),
|
|
]);
|
|
expect(common.status).toBe("in_progress");
|
|
expect(common.priority).toBe("high");
|
|
expect(common.assignee).toEqual({ type: "member", id: "u-1" });
|
|
});
|
|
|
|
it("returns the shared status when every issue agrees, not a hardcoded default", () => {
|
|
// Regression for MUL-3510: the batch picker used to assert "todo"
|
|
// regardless of the selection.
|
|
const common = commonIssueFields([
|
|
makeIssue({ id: "a", status: "in_review" }),
|
|
makeIssue({ id: "b", status: "in_review" }),
|
|
]);
|
|
expect(common.status).toBe("in_review");
|
|
});
|
|
|
|
it("returns null status when the selection spans different statuses (mixed)", () => {
|
|
const common = commonIssueFields([
|
|
makeIssue({ id: "a", status: "todo" }),
|
|
makeIssue({ id: "b", status: "done" }),
|
|
]);
|
|
expect(common.status).toBeNull();
|
|
});
|
|
|
|
it("derives each field independently — shared status, mixed priority", () => {
|
|
const common = commonIssueFields([
|
|
makeIssue({ id: "a", status: "blocked", priority: "urgent" }),
|
|
makeIssue({ id: "b", status: "blocked", priority: "low" }),
|
|
]);
|
|
expect(common.status).toBe("blocked");
|
|
expect(common.priority).toBeNull();
|
|
});
|
|
|
|
it("treats an all-unassigned selection as a real shared value, not mixed", () => {
|
|
const common = commonIssueFields([
|
|
makeIssue({ id: "a", assignee_type: null, assignee_id: null }),
|
|
makeIssue({ id: "b", assignee_type: null, assignee_id: null }),
|
|
]);
|
|
expect(common.assignee).toEqual({ type: null, id: null });
|
|
});
|
|
|
|
it("returns the shared assignee when every issue points at the same actor", () => {
|
|
const common = commonIssueFields([
|
|
makeIssue({ id: "a", assignee_type: "agent", assignee_id: "agent-1" }),
|
|
makeIssue({ id: "b", assignee_type: "agent", assignee_id: "agent-1" }),
|
|
]);
|
|
expect(common.assignee).toEqual({ type: "agent", id: "agent-1" });
|
|
});
|
|
|
|
it("returns null assignee when actors differ", () => {
|
|
const common = commonIssueFields([
|
|
makeIssue({ id: "a", assignee_type: "member", assignee_id: "u-1" }),
|
|
makeIssue({ id: "b", assignee_type: "member", assignee_id: "u-2" }),
|
|
]);
|
|
expect(common.assignee).toBeNull();
|
|
});
|
|
|
|
it("returns null assignee when some are assigned and some are unassigned", () => {
|
|
const common = commonIssueFields([
|
|
makeIssue({ id: "a", assignee_type: "member", assignee_id: "u-1" }),
|
|
makeIssue({ id: "b", assignee_type: null, assignee_id: null }),
|
|
]);
|
|
expect(common.assignee).toBeNull();
|
|
});
|
|
|
|
it("distinguishes assignees of the same id but different type", () => {
|
|
const common = commonIssueFields([
|
|
makeIssue({ id: "a", assignee_type: "member", assignee_id: "x" }),
|
|
makeIssue({ id: "b", assignee_type: "agent", assignee_id: "x" }),
|
|
]);
|
|
expect(common.assignee).toBeNull();
|
|
});
|
|
});
|