Files
multica/packages/views/issues/surface/use-issue-surface-actions.ts
Multica Eve 40f9ecdd56 MUL-5202: unify Issue Query across List, Board, and Swimlane (#5820)
* MUL-5202: migrate status issue surfaces to table query

Co-authored-by: multica-agent <github@multica.ai>

* MUL-5202: unify grouped issue surfaces

Co-authored-by: multica-agent <github@multica.ai>

* MUL-5202: cover move safety boundaries

Co-authored-by: multica-agent <github@multica.ai>

* MUL-5202: preserve server swimlane semantics

Co-authored-by: multica-agent <github@multica.ai>

* MUL-5202: keep grouped surface facets exact

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-23 15:58:25 +08:00

147 lines
3.8 KiB
TypeScript

"use client";
import { useCallback, useMemo } from "react";
import { toast } from "sonner";
import type { UpdateIssueRequest } from "@multica/core/types";
import {
useBatchDeleteIssues,
useBatchUpdateIssues,
useUpdateIssue,
} from "@multica/core/issues/mutations";
import { useModalStore } from "@multica/core/modals";
import {
type IssueSurfaceActions,
type IssueSurfaceMutationOptions,
} from "./actions-context";
import type { IssueCreateDefaults } from "./types";
import { useT } from "../../i18n";
export type MoveIssueUpdates = Pick<
UpdateIssueRequest,
| "status"
| "assignee_type"
| "assignee_id"
| "position"
| "parent_issue_id"
| "project_id"
> & {
before_id: string | null;
after_id: string | null;
};
export interface IssueSurfaceActionController {
actions: IssueSurfaceActions;
openCreateIssue: (defaults?: IssueCreateDefaults) => void;
moveIssue: (
issueId: string,
updates: MoveIssueUpdates,
onSettled?: () => void,
) => void;
}
export function useIssueSurfaceActions({
createDefaults,
}: {
createDefaults: IssueCreateDefaults;
}): IssueSurfaceActionController {
const { t } = useT("projects");
const updateIssueMutation = useUpdateIssue();
const batchUpdateMutation = useBatchUpdateIssues();
const batchDeleteMutation = useBatchDeleteIssues();
const updateIssue = useCallback(
(
issueId: string,
updates: Partial<UpdateIssueRequest>,
options?: IssueSurfaceMutationOptions,
) => {
updateIssueMutation.mutate(
{ id: issueId, ...updates },
{
onSuccess: () => options?.onSuccess?.(),
onError: (err) => {
toast.error(
err instanceof Error && err.message
? err.message
: (options?.errorMessage ??
t(($) => $.detail.toast_move_issue_failed)),
);
options?.onError?.(err);
},
onSettled: () => options?.onSettled?.(),
},
);
},
[t, updateIssueMutation],
);
const moveIssue = useCallback(
(
issueId: string,
updates: MoveIssueUpdates,
onSettled?: () => void,
) => {
const { before_id, after_id, ...optimisticUpdates } = updates;
updateIssueMutation.mutate(
{
id: issueId,
...optimisticUpdates,
move_intent: { before_id, after_id },
},
{
onError: (err) => {
toast.error(
err instanceof Error && err.message
? err.message
: t(($) => $.detail.toast_move_issue_failed),
);
},
onSettled,
},
);
},
[t, updateIssueMutation],
);
const openCreateIssue = useCallback(
(defaults?: IssueCreateDefaults) => {
useModalStore
.getState()
.open("create-issue", { ...createDefaults, ...defaults });
},
[createDefaults],
);
const actions = useMemo<IssueSurfaceActions>(
() => ({
isPending:
updateIssueMutation.isPending ||
batchUpdateMutation.isPending ||
batchDeleteMutation.isPending,
createIssue: openCreateIssue,
updateIssue,
moveIssue: (issueId, updates, options) =>
updateIssue(issueId, updates, {
errorMessage: t(($) => $.detail.toast_move_issue_failed),
...options,
}),
batchUpdate: async (issueIds, updates) => {
await batchUpdateMutation.mutateAsync({ ids: issueIds, updates });
},
batchDelete: async (issueIds) => {
await batchDeleteMutation.mutateAsync(issueIds);
},
}),
[
batchDeleteMutation,
batchUpdateMutation,
openCreateIssue,
t,
updateIssue,
updateIssueMutation.isPending,
],
);
return { actions, openCreateIssue, moveIssue };
}