mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-18 20:29:21 +02:00
Compare commits
1 Commits
quick-crea
...
agent/lamb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee7a440465 |
41
packages/core/feedback/draft-store.ts
Normal file
41
packages/core/feedback/draft-store.ts
Normal file
@@ -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<FeedbackDraft>) => void;
|
||||
clearDraft: () => void;
|
||||
hasDraft: () => boolean;
|
||||
}
|
||||
|
||||
export const useFeedbackDraftStore = create<FeedbackDraftStore>()(
|
||||
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());
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./mutations";
|
||||
export { useFeedbackDraftStore } from "./draft-store";
|
||||
|
||||
54
packages/core/projects/draft-store.ts
Normal file
54
packages/core/projects/draft-store.ts
Normal file
@@ -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<ProjectDraft>) => void;
|
||||
clearDraft: () => void;
|
||||
hasDraft: () => boolean;
|
||||
}
|
||||
|
||||
export const useProjectDraftStore = create<ProjectDraftStore>()(
|
||||
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());
|
||||
@@ -1,2 +1,3 @@
|
||||
export { projectKeys, projectListOptions, projectDetailOptions } from "./queries";
|
||||
export { useCreateProject, useUpdateProject, useDeleteProject } from "./mutations";
|
||||
export { useProjectDraftStore } from "./draft-store";
|
||||
|
||||
@@ -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<ContentEditorRef>(null);
|
||||
const [status, setStatus] = useState<ProjectStatus>("planned");
|
||||
const [priority, setPriority] = useState<ProjectPriority>("none");
|
||||
const [leadType, setLeadType] = useState<"member" | "agent" | undefined>();
|
||||
const [leadId, setLeadId] = useState<string | undefined>();
|
||||
const [icon, setIcon] = useState<string | undefined>();
|
||||
const [status, setStatus] = useState<ProjectStatus>(draft.status);
|
||||
const [priority, setPriority] = useState<ProjectPriority>(draft.priority);
|
||||
const [leadType, setLeadType] = useState<"member" | "agent" | undefined>(draft.leadType);
|
||||
const [leadId, setLeadId] = useState<string | undefined>(draft.leadId);
|
||||
const [icon, setIcon] = useState<string | undefined>(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 }) {
|
||||
<PopoverContent align="start" className="w-auto p-0">
|
||||
<EmojiPicker
|
||||
onSelect={(emoji) => {
|
||||
setIcon(emoji);
|
||||
updateIcon(emoji);
|
||||
setIconPickerOpen(false);
|
||||
}}
|
||||
/>
|
||||
@@ -185,10 +201,10 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
</Popover>
|
||||
<TitleEditor
|
||||
autoFocus
|
||||
defaultValue=""
|
||||
defaultValue={draft.title}
|
||||
placeholder="Project title"
|
||||
className="text-lg font-semibold"
|
||||
onChange={(v) => setTitle(v)}
|
||||
onChange={(v) => updateTitle(v)}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
@@ -196,8 +212,9 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
<div className="flex-1 min-h-0 overflow-y-auto px-5">
|
||||
<ContentEditor
|
||||
ref={descEditorRef}
|
||||
defaultValue=""
|
||||
defaultValue={draft.description}
|
||||
placeholder="Add description..."
|
||||
onUpdate={(md) => setDraft({ description: md })}
|
||||
debounceMs={500}
|
||||
/>
|
||||
</div>
|
||||
@@ -214,7 +231,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
/>
|
||||
<DropdownMenuContent align="start" className="w-44">
|
||||
{PROJECT_STATUS_ORDER.map((s) => (
|
||||
<DropdownMenuItem key={s} onClick={() => setStatus(s)}>
|
||||
<DropdownMenuItem key={s} onClick={() => updateStatus(s)}>
|
||||
<span className={cn("size-2 rounded-full", PROJECT_STATUS_CONFIG[s].dotColor)} />
|
||||
<span>{PROJECT_STATUS_CONFIG[s].label}</span>
|
||||
</DropdownMenuItem>
|
||||
@@ -233,7 +250,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
/>
|
||||
<DropdownMenuContent align="start" className="w-44">
|
||||
{PROJECT_PRIORITY_ORDER.map((pr) => (
|
||||
<DropdownMenuItem key={pr} onClick={() => setPriority(pr)}>
|
||||
<DropdownMenuItem key={pr} onClick={() => updatePriority(pr)}>
|
||||
<PriorityIcon priority={pr} />
|
||||
<span>{PROJECT_PRIORITY_CONFIG[pr].label}</span>
|
||||
</DropdownMenuItem>
|
||||
@@ -276,8 +293,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setLeadType(undefined);
|
||||
setLeadId(undefined);
|
||||
updateLead(undefined, undefined);
|
||||
setLeadOpen(false);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
||||
@@ -295,8 +311,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
type="button"
|
||||
key={m.user_id}
|
||||
onClick={() => {
|
||||
setLeadType("member");
|
||||
setLeadId(m.user_id);
|
||||
updateLead("member", m.user_id);
|
||||
setLeadOpen(false);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
||||
@@ -317,8 +332,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
type="button"
|
||||
key={a.id}
|
||||
onClick={() => {
|
||||
setLeadType("agent");
|
||||
setLeadId(a.id);
|
||||
updateLead("agent", a.id);
|
||||
setLeadOpen(false);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
useFileDropZone,
|
||||
FileDropOverlay,
|
||||
} from "../editor";
|
||||
import { useCreateFeedback } from "@multica/core/feedback";
|
||||
import { useCreateFeedback, useFeedbackDraftStore } from "@multica/core/feedback";
|
||||
import { useCurrentWorkspace } from "@multica/core/paths";
|
||||
import { useFileUpload } from "@multica/core/hooks/use-file-upload";
|
||||
import { api } from "@multica/core/api";
|
||||
@@ -26,8 +26,12 @@ const MAX_MESSAGE_LEN = 10000;
|
||||
|
||||
export function FeedbackModal({ onClose }: { onClose: () => void }) {
|
||||
const workspace = useCurrentWorkspace();
|
||||
const draft = useFeedbackDraftStore((s) => s.draft);
|
||||
const setDraft = useFeedbackDraftStore((s) => s.setDraft);
|
||||
const clearDraft = useFeedbackDraftStore((s) => s.clearDraft);
|
||||
|
||||
const editorRef = useRef<ContentEditorRef>(null);
|
||||
const [message, setMessage] = useState("");
|
||||
const [message, setMessage] = useState(draft.message);
|
||||
const { isDragOver, dropZoneProps } = useFileDropZone({
|
||||
onDrop: (files) => files.forEach((f) => editorRef.current?.uploadFile(f)),
|
||||
});
|
||||
@@ -69,6 +73,7 @@ export function FeedbackModal({ onClose }: { onClose: () => void }) {
|
||||
url: typeof window !== "undefined" ? window.location.href : undefined,
|
||||
workspace_id: workspace?.id,
|
||||
});
|
||||
clearDraft();
|
||||
toast.success("Thanks for the feedback!");
|
||||
onClose();
|
||||
} catch (err) {
|
||||
@@ -98,8 +103,9 @@ export function FeedbackModal({ onClose }: { onClose: () => void }) {
|
||||
>
|
||||
<ContentEditor
|
||||
ref={editorRef}
|
||||
defaultValue={draft.message}
|
||||
placeholder="Tell us about your experience, bugs you've found, or features you'd like to see…"
|
||||
onUpdate={(md) => setMessage(md)}
|
||||
onUpdate={(md) => { setMessage(md); setDraft({ message: md }); }}
|
||||
onUploadFile={uploadWithToast}
|
||||
onSubmit={handleSubmit}
|
||||
debounceMs={150}
|
||||
|
||||
Reference in New Issue
Block a user