diff --git a/packages/core/feedback/draft-store.ts b/packages/core/feedback/draft-store.ts new file mode 100644 index 000000000..91587b37b --- /dev/null +++ b/packages/core/feedback/draft-store.ts @@ -0,0 +1,41 @@ +import { create } from "zustand"; +import { createJSONStorage, persist } from "zustand/middleware"; +import { createWorkspaceAwareStorage, registerForWorkspaceRehydration } from "../platform/workspace-storage"; +import { defaultStorage } from "../platform/storage"; + +interface FeedbackDraft { + message: string; +} + +const EMPTY_DRAFT: FeedbackDraft = { + message: "", +}; + +interface FeedbackDraftStore { + draft: FeedbackDraft; + setDraft: (patch: Partial) => void; + clearDraft: () => void; + hasDraft: () => boolean; +} + +export const useFeedbackDraftStore = create()( + persist( + (set, get) => ({ + draft: { ...EMPTY_DRAFT }, + setDraft: (patch) => + set((s) => ({ draft: { ...s.draft, ...patch } })), + clearDraft: () => + set({ draft: { ...EMPTY_DRAFT } }), + hasDraft: () => { + const { draft } = get(); + return !!draft.message; + }, + }), + { + name: "multica_feedback_draft", + storage: createJSONStorage(() => createWorkspaceAwareStorage(defaultStorage)), + }, + ), +); + +registerForWorkspaceRehydration(() => useFeedbackDraftStore.persist.rehydrate()); diff --git a/packages/core/feedback/index.ts b/packages/core/feedback/index.ts index f8dd99d03..105523c16 100644 --- a/packages/core/feedback/index.ts +++ b/packages/core/feedback/index.ts @@ -1 +1,2 @@ export * from "./mutations"; +export { useFeedbackDraftStore } from "./draft-store"; diff --git a/packages/core/projects/draft-store.ts b/packages/core/projects/draft-store.ts new file mode 100644 index 000000000..8b10b9768 --- /dev/null +++ b/packages/core/projects/draft-store.ts @@ -0,0 +1,54 @@ +import { create } from "zustand"; +import { createJSONStorage, persist } from "zustand/middleware"; +import type { ProjectStatus, ProjectPriority } from "../types"; +import { createWorkspaceAwareStorage, registerForWorkspaceRehydration } from "../platform/workspace-storage"; +import { defaultStorage } from "../platform/storage"; + +interface ProjectDraft { + title: string; + description: string; + status: ProjectStatus; + priority: ProjectPriority; + leadType?: "member" | "agent"; + leadId?: string; + icon?: string; +} + +const EMPTY_DRAFT: ProjectDraft = { + title: "", + description: "", + status: "planned", + priority: "none", + leadType: undefined, + leadId: undefined, + icon: undefined, +}; + +interface ProjectDraftStore { + draft: ProjectDraft; + setDraft: (patch: Partial) => void; + clearDraft: () => void; + hasDraft: () => boolean; +} + +export const useProjectDraftStore = create()( + persist( + (set, get) => ({ + draft: { ...EMPTY_DRAFT }, + setDraft: (patch) => + set((s) => ({ draft: { ...s.draft, ...patch } })), + clearDraft: () => + set({ draft: { ...EMPTY_DRAFT } }), + hasDraft: () => { + const { draft } = get(); + return !!(draft.title || draft.description); + }, + }), + { + name: "multica_project_draft", + storage: createJSONStorage(() => createWorkspaceAwareStorage(defaultStorage)), + }, + ), +); + +registerForWorkspaceRehydration(() => useProjectDraftStore.persist.rehydrate()); diff --git a/packages/core/projects/index.ts b/packages/core/projects/index.ts index 1e63d3c13..9e5c466cd 100644 --- a/packages/core/projects/index.ts +++ b/packages/core/projects/index.ts @@ -1,2 +1,3 @@ export { projectKeys, projectListOptions, projectDetailOptions } from "./queries"; export { useCreateProject, useUpdateProject, useDeleteProject } from "./mutations"; +export { useProjectDraftStore } from "./draft-store"; diff --git a/packages/views/modals/create-project.tsx b/packages/views/modals/create-project.tsx index 8483fcbf6..e0d33ff97 100644 --- a/packages/views/modals/create-project.tsx +++ b/packages/views/modals/create-project.tsx @@ -4,6 +4,7 @@ import { useState, useRef } from "react"; import { ChevronRight, Maximize2, Minimize2, X as XIcon, UserMinus } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { useCreateProject } from "@multica/core/projects/mutations"; +import { useProjectDraftStore } from "@multica/core/projects"; import { PROJECT_STATUS_CONFIG, PROJECT_STATUS_ORDER, @@ -63,17 +64,31 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { const { data: agents = [] } = useQuery(agentListOptions(wsId)); const { getActorName } = useActorName(); - const [title, setTitle] = useState(""); + const draft = useProjectDraftStore((s) => s.draft); + const setDraft = useProjectDraftStore((s) => s.setDraft); + const clearDraft = useProjectDraftStore((s) => s.clearDraft); + + const [title, setTitle] = useState(draft.title); const descEditorRef = useRef(null); - const [status, setStatus] = useState("planned"); - const [priority, setPriority] = useState("none"); - const [leadType, setLeadType] = useState<"member" | "agent" | undefined>(); - const [leadId, setLeadId] = useState(); - const [icon, setIcon] = useState(); + const [status, setStatus] = useState(draft.status); + const [priority, setPriority] = useState(draft.priority); + const [leadType, setLeadType] = useState<"member" | "agent" | undefined>(draft.leadType); + const [leadId, setLeadId] = useState(draft.leadId); + const [icon, setIcon] = useState(draft.icon); const [iconPickerOpen, setIconPickerOpen] = useState(false); const [submitting, setSubmitting] = useState(false); const [isExpanded, setIsExpanded] = useState(false); + // Sync field changes to draft store + const updateTitle = (v: string) => { setTitle(v); setDraft({ title: v }); }; + const updateStatus = (v: ProjectStatus) => { setStatus(v); setDraft({ status: v }); }; + const updatePriority = (v: ProjectPriority) => { setPriority(v); setDraft({ priority: v }); }; + const updateLead = (type?: "member" | "agent", id?: string) => { + setLeadType(type); setLeadId(id); + setDraft({ leadType: type, leadId: id }); + }; + const updateIcon = (v: string | undefined) => { setIcon(v); setDraft({ icon: v }); }; + const [leadOpen, setLeadOpen] = useState(false); const [leadFilter, setLeadFilter] = useState(""); @@ -100,6 +115,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { lead_type: leadType, lead_id: leadId, }); + clearDraft(); onClose(); toast.success("Project created"); router.push(wsPaths.projectDetail(project.id)); @@ -177,7 +193,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { { - setIcon(emoji); + updateIcon(emoji); setIconPickerOpen(false); }} /> @@ -185,10 +201,10 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { setTitle(v)} + onChange={(v) => updateTitle(v)} onSubmit={handleSubmit} /> @@ -196,8 +212,9 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
setDraft({ description: md })} debounceMs={500} />
@@ -214,7 +231,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { /> {PROJECT_STATUS_ORDER.map((s) => ( - setStatus(s)}> + updateStatus(s)}> {PROJECT_STATUS_CONFIG[s].label} @@ -233,7 +250,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) { /> {PROJECT_PRIORITY_ORDER.map((pr) => ( - setPriority(pr)}> + updatePriority(pr)}> {PROJECT_PRIORITY_CONFIG[pr].label} @@ -276,8 +293,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {