85 lines
3.1 KiB
TypeScript
85 lines
3.1 KiB
TypeScript
/**
|
|
* Draft state for the New Issue modal (`app/(app)/[workspace]/new-issue.tsx`).
|
|
*
|
|
* Why a store instead of local useState: the formSheet picker routes
|
|
* (`new-issue-picker/status.tsx`, etc.) live in a separate Stack screen and
|
|
* have no React parent-child relationship with the new-issue modal. They
|
|
* need a way to read the current draft value and write the new selection
|
|
* back without prop-drilling through the router. A small Zustand store is
|
|
* the minimum-viable cross-screen channel.
|
|
*
|
|
* Lifecycle: `reset()` runs from `new-issue.tsx` when the user dismisses
|
|
* the modal (either submit succeeds or they cancel) so the next open
|
|
* starts clean. Seed-from-comment params still go through local useState
|
|
* inside the screen (description text is a controlled input that doesn't
|
|
* cross routes); only the attribute-chip values live here.
|
|
*
|
|
* Workspace lifecycle: this draft is workspace-scoped (e.g. an `assignee`
|
|
* id only resolves in the workspace whose memberlist seeded it). When the
|
|
* user switches workspaces, the draft is invalid. Reset is wired in
|
|
* `app/(app)/[workspace]/_layout.tsx` via `useResetOnWorkspaceChange()` —
|
|
* that's the only place that calls it on workspace-id transitions.
|
|
*/
|
|
import { useEffect, useRef } from "react";
|
|
import { create } from "zustand";
|
|
import type {
|
|
IssuePriority,
|
|
IssueStatus,
|
|
Project,
|
|
} from "@multica/core/types";
|
|
import type { AssigneeValue } from "@/components/issue/pickers/assignee-picker-body";
|
|
|
|
interface NewIssueDraftState {
|
|
status: IssueStatus;
|
|
priority: IssuePriority;
|
|
assignee: AssigneeValue;
|
|
dueDate: string | null;
|
|
project: Project | null;
|
|
setStatus: (next: IssueStatus) => void;
|
|
setPriority: (next: IssuePriority) => void;
|
|
setAssignee: (next: AssigneeValue) => void;
|
|
setDueDate: (next: string | null) => void;
|
|
setProject: (next: Project | null) => void;
|
|
reset: () => void;
|
|
}
|
|
|
|
const INITIAL: Pick<
|
|
NewIssueDraftState,
|
|
"status" | "priority" | "assignee" | "dueDate" | "project"
|
|
> = {
|
|
status: "todo",
|
|
priority: "none",
|
|
assignee: null,
|
|
dueDate: null,
|
|
project: null,
|
|
};
|
|
|
|
export const useNewIssueDraftStore = create<NewIssueDraftState>((set) => ({
|
|
...INITIAL,
|
|
setStatus: (next) => set({ status: next }),
|
|
setPriority: (next) => set({ priority: next }),
|
|
setAssignee: (next) => set({ assignee: next }),
|
|
setDueDate: (next) => set({ dueDate: next }),
|
|
setProject: (next) => set({ project: next }),
|
|
reset: () => set({ ...INITIAL }),
|
|
}));
|
|
|
|
/**
|
|
* Clears the new-issue draft store whenever the active workspace id
|
|
* changes. Mounted once from the workspace `_layout.tsx`; relies on the
|
|
* workspace store being the source of truth. The `useRef` gate ensures
|
|
* the first mount is a no-op — we only fire `reset()` when the id
|
|
* actually changes from one value to another, so a fresh app launch that
|
|
* resolves the workspace into a non-null id doesn't pointlessly stomp
|
|
* the already-INITIAL store on every cold start.
|
|
*/
|
|
export function useNewIssueDraftResetOnWorkspaceChange(wsId: string | null) {
|
|
const prevRef = useRef(wsId);
|
|
useEffect(() => {
|
|
if (prevRef.current !== wsId) {
|
|
useNewIssueDraftStore.getState().reset();
|
|
prevRef.current = wsId;
|
|
}
|
|
}, [wsId]);
|
|
}
|