mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 14:37:44 +02:00
* refactor(repos): drop unused description + tighten create-project layout
Two related changes that touch the workspace-repos surface together.
1. Remove the per-repo `description` field everywhere it was threaded.
The only place it ever surfaced was a markdown table column the daemon
wrote into the agent runtime config, where most rows just read "—"
anyway. Agents already discover project structure by running
`multica project` / `multica issue` against the CLI, so the human-
readable description string carried no real value while taking up an
extra Settings input row and propagating through six layers (settings
UI → workspace.repos jsonb → handler RepoData → daemon RepoData →
repocache.RepoInfo → execenv.RepoContextForEnv).
- Settings → Repositories drops the description input; the URL field
now spans the whole row.
- WorkspaceRepo TS type loses `description`; backend RepoData /
RepoInfo / RepoContextForEnv all collapse to URL only.
- Daemon's runtime_config Repositories block changes from a
`| URL | Description |` markdown table to a simple bullet list.
- Tests updated; jsonb residue in existing workspaces is dropped at
normalize time, so no migration needed.
2. Tighten the Create Project modal footer: pull the Status / Priority /
Lead / Repos pills onto the same row as the Create Project button
(Linear-style single-row footer) instead of stacking them above it,
and swap the Repos pill icon from `FolderGit` to a real GitHub mark
(lucide-react v1 dropped brand icons, so the mark lives inline as a
small SVG component in this file).
I tried promoting Repos to its own "Resources" strip above the footer
to separate the resources abstraction from project metadata, but with
a single pill it looked too sparse — leaving a TODO comment in the
footer to revisit once we add Linear / Notion / Figma / Slack
resource types.
* fix(daemon test): drop residual Description field on RepoData literals
* fix(repos): drop Description residue surfaced after rebase on #1929
Project-resource github_repo lift path (#1929) and registerTaskRepos
both still constructed RepoData{...Description: ...} after the rebase.
Two test sites in daemon_test.go and execenv_test.go also reintroduced
the field. Strip them so the Description-removal change builds and
tests pass with the latest main.
524 lines
21 KiB
TypeScript
524 lines
21 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useRef } from "react";
|
||
import { ChevronRight, Maximize2, Minimize2, X as XIcon, UserMinus } from "lucide-react";
|
||
|
||
/**
|
||
* GitHub mark — lucide-react v1 dropped brand icons, so we inline the
|
||
* Octicon-style mark here (24×24 viewBox, currentColor fill so it inherits
|
||
* the parent's text color). Stays in this file because there's only one
|
||
* caller; promote to packages/ui if a second use crops up.
|
||
*/
|
||
function GithubIcon({ className }: { className?: string }) {
|
||
return (
|
||
<svg
|
||
viewBox="0 0 24 24"
|
||
fill="currentColor"
|
||
aria-hidden="true"
|
||
className={className}
|
||
>
|
||
<path d="M12 .5C5.73.5.66 5.57.66 11.84c0 5.01 3.25 9.26 7.76 10.76.57.1.78-.25.78-.55 0-.27-.01-1.17-.02-2.13-3.16.69-3.83-1.34-3.83-1.34-.52-1.31-1.27-1.66-1.27-1.66-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.75 2.68 1.24 3.34.95.1-.74.4-1.24.72-1.53-2.52-.29-5.18-1.26-5.18-5.62 0-1.24.45-2.26 1.18-3.06-.12-.29-.51-1.45.11-3.02 0 0 .96-.31 3.15 1.17a10.93 10.93 0 0 1 5.74 0c2.19-1.48 3.15-1.17 3.15-1.17.62 1.57.23 2.73.11 3.02.74.8 1.18 1.82 1.18 3.06 0 4.37-2.67 5.32-5.21 5.61.41.35.78 1.04.78 2.1 0 1.52-.01 2.74-.01 3.11 0 .3.21.66.79.55 4.51-1.5 7.76-5.75 7.76-10.76C23.34 5.57 18.27.5 12 .5Z" />
|
||
</svg>
|
||
);
|
||
}
|
||
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,
|
||
PROJECT_PRIORITY_CONFIG,
|
||
PROJECT_PRIORITY_ORDER,
|
||
} from "@multica/core/projects/config";
|
||
import { useWorkspaceId } from "@multica/core/hooks";
|
||
import { useCurrentWorkspace, useWorkspacePaths } from "@multica/core/paths";
|
||
import { memberListOptions, agentListOptions } from "@multica/core/workspace/queries";
|
||
import { useActorName } from "@multica/core/workspace/hooks";
|
||
import type { ProjectStatus, ProjectPriority } from "@multica/core/types";
|
||
import { cn } from "@multica/ui/lib/utils";
|
||
import { toast } from "sonner";
|
||
import { Dialog, DialogContent, DialogTitle } from "@multica/ui/components/ui/dialog";
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuTrigger,
|
||
} from "@multica/ui/components/ui/dropdown-menu";
|
||
import { Popover, PopoverTrigger, PopoverContent } from "@multica/ui/components/ui/popover";
|
||
import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/ui/tooltip";
|
||
import { Button } from "@multica/ui/components/ui/button";
|
||
import { EmojiPicker } from "@multica/ui/components/common/emoji-picker";
|
||
import { ContentEditor, type ContentEditorRef, TitleEditor } from "../editor";
|
||
import { PriorityIcon } from "../issues/components/priority-icon";
|
||
import { ActorAvatar } from "../common/actor-avatar";
|
||
import { useNavigation } from "../navigation";
|
||
|
||
function PillButton({
|
||
children,
|
||
className,
|
||
...props
|
||
}: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||
return (
|
||
<button
|
||
type="button"
|
||
className={cn(
|
||
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs",
|
||
"hover:bg-accent/60 transition-colors cursor-pointer",
|
||
className,
|
||
)}
|
||
{...props}
|
||
>
|
||
{children}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||
const router = useNavigation();
|
||
const workspace = useCurrentWorkspace();
|
||
const workspaceName = workspace?.name;
|
||
const wsPaths = useWorkspacePaths();
|
||
const wsId = useWorkspaceId();
|
||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||
const { data: agents = [] } = useQuery(agentListOptions(wsId));
|
||
const { getActorName } = useActorName();
|
||
|
||
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>(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);
|
||
// Repos selected to attach as github_repo resources after the project is
|
||
// created. Stored as URLs (not full ProjectResource rows) — they're not
|
||
// persisted until handleSubmit fires the createProjectResource calls.
|
||
const [selectedRepos, setSelectedRepos] = useState<string[]>([]);
|
||
const [repoPopoverOpen, setRepoPopoverOpen] = useState(false);
|
||
const [customRepoUrl, setCustomRepoUrl] = useState("");
|
||
const workspaceRepos = workspace?.repos ?? [];
|
||
|
||
// 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("");
|
||
|
||
const leadQuery = leadFilter.toLowerCase();
|
||
const filteredMembers = members.filter((m) => m.name.toLowerCase().includes(leadQuery));
|
||
const filteredAgents = agents.filter(
|
||
(a) => !a.archived_at && a.name.toLowerCase().includes(leadQuery),
|
||
);
|
||
|
||
const leadLabel = leadType && leadId ? getActorName(leadType, leadId) : "Lead";
|
||
|
||
const createProject = useCreateProject();
|
||
|
||
const handleSubmit = async () => {
|
||
if (!title.trim() || submitting) return;
|
||
setSubmitting(true);
|
||
try {
|
||
const project = await createProject.mutateAsync({
|
||
title: title.trim(),
|
||
description: descEditorRef.current?.getMarkdown()?.trim() || undefined,
|
||
icon,
|
||
status,
|
||
priority,
|
||
lead_type: leadType,
|
||
lead_id: leadId,
|
||
// Server attaches these in the same transaction as the project.
|
||
resources:
|
||
selectedRepos.length > 0
|
||
? selectedRepos.map((url) => ({
|
||
resource_type: "github_repo" as const,
|
||
resource_ref: { url },
|
||
}))
|
||
: undefined,
|
||
});
|
||
clearDraft();
|
||
onClose();
|
||
toast.success("Project created");
|
||
router.push(wsPaths.projectDetail(project.id));
|
||
} catch {
|
||
toast.error("Failed to create project");
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const toggleRepo = (url: string) => {
|
||
setSelectedRepos((prev) =>
|
||
prev.includes(url) ? prev.filter((u) => u !== url) : [...prev, url],
|
||
);
|
||
};
|
||
|
||
const addCustomRepo = () => {
|
||
const url = customRepoUrl.trim();
|
||
if (!url) return;
|
||
setSelectedRepos((prev) => (prev.includes(url) ? prev : [...prev, url]));
|
||
setCustomRepoUrl("");
|
||
};
|
||
|
||
return (
|
||
<Dialog open onOpenChange={(v) => { if (!v) onClose(); }}>
|
||
<DialogContent
|
||
showCloseButton={false}
|
||
className={cn(
|
||
"p-0 gap-0 flex flex-col overflow-hidden",
|
||
"!top-1/2 !left-1/2 !-translate-x-1/2",
|
||
"!transition-all !duration-300 !ease-out",
|
||
isExpanded
|
||
? "!max-w-4xl !w-full !h-5/6 !-translate-y-1/2"
|
||
: "!max-w-2xl !w-full !h-96 !-translate-y-1/2",
|
||
)}
|
||
>
|
||
<DialogTitle className="sr-only">New Project</DialogTitle>
|
||
|
||
<div className="flex items-center justify-between px-5 pt-3 pb-2 shrink-0">
|
||
<div className="flex items-center gap-1.5 text-xs">
|
||
<span className="text-muted-foreground">{workspaceName}</span>
|
||
<ChevronRight className="size-3 text-muted-foreground/50" />
|
||
<span className="font-medium">New project</span>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<Tooltip>
|
||
<TooltipTrigger
|
||
render={
|
||
<button
|
||
onClick={() => setIsExpanded(!isExpanded)}
|
||
className="rounded-sm p-1.5 opacity-70 hover:opacity-100 hover:bg-accent/60 transition-all cursor-pointer"
|
||
>
|
||
{isExpanded ? <Minimize2 className="size-4" /> : <Maximize2 className="size-4" />}
|
||
</button>
|
||
}
|
||
/>
|
||
<TooltipContent side="bottom">{isExpanded ? "Collapse" : "Expand"}</TooltipContent>
|
||
</Tooltip>
|
||
<Tooltip>
|
||
<TooltipTrigger
|
||
render={
|
||
<button
|
||
onClick={onClose}
|
||
className="rounded-sm p-1.5 opacity-70 hover:opacity-100 hover:bg-accent/60 transition-all cursor-pointer"
|
||
>
|
||
<XIcon className="size-4" />
|
||
</button>
|
||
}
|
||
/>
|
||
<TooltipContent side="bottom">Close</TooltipContent>
|
||
</Tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="px-5 pb-2 shrink-0">
|
||
<Popover open={iconPickerOpen} onOpenChange={setIconPickerOpen}>
|
||
<PopoverTrigger
|
||
render={
|
||
<button
|
||
type="button"
|
||
className="text-2xl cursor-pointer rounded-lg p-1 -ml-1 hover:bg-accent/60 transition-colors"
|
||
title="Choose icon"
|
||
>
|
||
{icon || "📁"}
|
||
</button>
|
||
}
|
||
/>
|
||
<PopoverContent align="start" className="w-auto p-0">
|
||
<EmojiPicker
|
||
onSelect={(emoji) => {
|
||
updateIcon(emoji);
|
||
setIconPickerOpen(false);
|
||
}}
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
<TitleEditor
|
||
autoFocus
|
||
defaultValue={draft.title}
|
||
placeholder="Project title"
|
||
className="text-lg font-semibold"
|
||
onChange={(v) => updateTitle(v)}
|
||
onSubmit={handleSubmit}
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex-1 min-h-0 overflow-y-auto px-5">
|
||
<ContentEditor
|
||
ref={descEditorRef}
|
||
defaultValue={draft.description}
|
||
placeholder="Add description..."
|
||
onUpdate={(md) => setDraft({ description: md })}
|
||
debounceMs={500}
|
||
/>
|
||
</div>
|
||
|
||
{/* Footer: properties (left, wrap) + Create button (right). Single row
|
||
so the modal stays compact — Linear-style.
|
||
Repos lives here alongside the property pills for now. Once we
|
||
support more resource types (Linear / Notion / Figma / Slack), pull
|
||
them out into a dedicated Resources strip above this footer — a
|
||
single Repos pill on its own row looked too sparse. */}
|
||
<div className="flex items-center justify-between gap-2 px-4 py-3 border-t shrink-0">
|
||
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger
|
||
render={
|
||
<PillButton>
|
||
<span className={cn("size-2 rounded-full", PROJECT_STATUS_CONFIG[status].dotColor)} />
|
||
<span>{PROJECT_STATUS_CONFIG[status].label}</span>
|
||
</PillButton>
|
||
}
|
||
/>
|
||
<DropdownMenuContent align="start" className="w-44">
|
||
{PROJECT_STATUS_ORDER.map((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>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger
|
||
render={
|
||
<PillButton>
|
||
<PriorityIcon priority={priority} />
|
||
<span>{PROJECT_PRIORITY_CONFIG[priority].label}</span>
|
||
</PillButton>
|
||
}
|
||
/>
|
||
<DropdownMenuContent align="start" className="w-44">
|
||
{PROJECT_PRIORITY_ORDER.map((pr) => (
|
||
<DropdownMenuItem key={pr} onClick={() => updatePriority(pr)}>
|
||
<PriorityIcon priority={pr} />
|
||
<span>{PROJECT_PRIORITY_CONFIG[pr].label}</span>
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
<Popover
|
||
open={leadOpen}
|
||
onOpenChange={(v) => {
|
||
setLeadOpen(v);
|
||
if (!v) setLeadFilter("");
|
||
}}
|
||
>
|
||
<PopoverTrigger
|
||
render={
|
||
<PillButton>
|
||
{leadType && leadId ? (
|
||
<>
|
||
<ActorAvatar actorType={leadType} actorId={leadId} size={16} showStatusDot />
|
||
<span>{leadLabel}</span>
|
||
</>
|
||
) : (
|
||
<span className="text-muted-foreground">Lead</span>
|
||
)}
|
||
</PillButton>
|
||
}
|
||
/>
|
||
<PopoverContent align="start" className="w-52 p-0">
|
||
<div className="px-2 py-1.5 border-b">
|
||
<input
|
||
type="text"
|
||
value={leadFilter}
|
||
onChange={(e) => setLeadFilter(e.target.value)}
|
||
placeholder="Assign lead..."
|
||
className="w-full bg-transparent text-sm placeholder:text-muted-foreground outline-none"
|
||
/>
|
||
</div>
|
||
<div className="p-1 max-h-60 overflow-y-auto">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
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"
|
||
>
|
||
<UserMinus className="h-3.5 w-3.5 text-muted-foreground" />
|
||
<span className="text-muted-foreground">No lead</span>
|
||
</button>
|
||
{filteredMembers.length > 0 && (
|
||
<>
|
||
<div className="px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||
Members
|
||
</div>
|
||
{filteredMembers.map((m) => (
|
||
<button
|
||
type="button"
|
||
key={m.user_id}
|
||
onClick={() => {
|
||
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"
|
||
>
|
||
<ActorAvatar actorType="member" actorId={m.user_id} size={16} />
|
||
<span>{m.name}</span>
|
||
</button>
|
||
))}
|
||
</>
|
||
)}
|
||
{filteredAgents.length > 0 && (
|
||
<>
|
||
<div className="px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||
Agents
|
||
</div>
|
||
{filteredAgents.map((a) => (
|
||
<button
|
||
type="button"
|
||
key={a.id}
|
||
onClick={() => {
|
||
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"
|
||
>
|
||
<ActorAvatar actorType="agent" actorId={a.id} size={16} showStatusDot />
|
||
<span>{a.name}</span>
|
||
</button>
|
||
))}
|
||
</>
|
||
)}
|
||
{filteredMembers.length === 0 &&
|
||
filteredAgents.length === 0 &&
|
||
leadFilter && (
|
||
<div className="px-2 py-3 text-center text-sm text-muted-foreground">
|
||
No results
|
||
</div>
|
||
)}
|
||
</div>
|
||
</PopoverContent>
|
||
</Popover>
|
||
|
||
<Popover open={repoPopoverOpen} onOpenChange={setRepoPopoverOpen}>
|
||
<PopoverTrigger
|
||
render={
|
||
<PillButton>
|
||
<GithubIcon className="size-3" />
|
||
<span>
|
||
{selectedRepos.length === 0
|
||
? "Repos"
|
||
: `${selectedRepos.length} repo${selectedRepos.length === 1 ? "" : "s"}`}
|
||
</span>
|
||
</PillButton>
|
||
}
|
||
/>
|
||
<PopoverContent align="start" className="w-72 p-2 space-y-2">
|
||
<div className="text-xs font-medium text-muted-foreground">
|
||
Attach GitHub repos to this project
|
||
</div>
|
||
{workspaceRepos.length > 0 ? (
|
||
<div className="space-y-1">
|
||
{workspaceRepos.map((repo) => {
|
||
const checked = selectedRepos.includes(repo.url);
|
||
return (
|
||
<button
|
||
type="button"
|
||
key={repo.url}
|
||
onClick={() => toggleRepo(repo.url)}
|
||
className={cn(
|
||
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-xs hover:bg-accent transition-colors",
|
||
checked && "bg-accent",
|
||
)}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={checked}
|
||
readOnly
|
||
className="size-3.5"
|
||
/>
|
||
<GithubIcon className="size-3.5" />
|
||
<span className="truncate flex-1 text-left">{repo.url}</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
<p className="text-xs text-muted-foreground">
|
||
No workspace-level repos yet. Paste a URL below to attach one
|
||
ad-hoc.
|
||
</p>
|
||
)}
|
||
<form
|
||
onSubmit={(e) => {
|
||
e.preventDefault();
|
||
addCustomRepo();
|
||
}}
|
||
className="flex items-center gap-1.5 pt-1 border-t"
|
||
>
|
||
<input
|
||
type="url"
|
||
value={customRepoUrl}
|
||
onChange={(e) => setCustomRepoUrl(e.target.value)}
|
||
placeholder="https://github.com/owner/repo"
|
||
className="flex-1 bg-transparent text-xs px-2 py-1 outline-none placeholder:text-muted-foreground"
|
||
/>
|
||
<Button
|
||
type="submit"
|
||
size="sm"
|
||
variant="ghost"
|
||
className="h-6 px-2 text-xs"
|
||
disabled={!customRepoUrl.trim()}
|
||
>
|
||
Add
|
||
</Button>
|
||
</form>
|
||
{selectedRepos.length > 0 && (
|
||
<div className="space-y-1 pt-1 border-t">
|
||
<div className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
||
Selected
|
||
</div>
|
||
{selectedRepos.map((url) => (
|
||
<div
|
||
key={url}
|
||
className="flex items-center gap-2 text-xs"
|
||
>
|
||
<GithubIcon className="size-3 text-muted-foreground" />
|
||
<span className="truncate flex-1">{url}</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => toggleRepo(url)}
|
||
className="text-muted-foreground hover:text-foreground"
|
||
>
|
||
<XIcon className="size-3" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</PopoverContent>
|
||
</Popover>
|
||
</div>
|
||
|
||
<Button
|
||
size="sm"
|
||
onClick={handleSubmit}
|
||
disabled={!title.trim() || submitting}
|
||
className="shrink-0"
|
||
>
|
||
{submitting ? "Creating..." : "Create Project"}
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|