feat: dialog to change template metadata in editor
This commit is contained in:
@@ -19,6 +19,7 @@ type createTemplateRequestBody struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type postTemplateRequestBody struct {
|
type postTemplateRequestBody struct {
|
||||||
|
Name *string `json:"name"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
Files []templateFile `json:"files"`
|
Files []templateFile `json:"files"`
|
||||||
|
|
||||||
@@ -114,13 +115,22 @@ func updateTemplate(c echo.Context, body postTemplateRequestBody) error {
|
|||||||
mgr := templateManagerFrom(c)
|
mgr := templateManagerFrom(c)
|
||||||
ctx := c.Request().Context()
|
ctx := c.Request().Context()
|
||||||
|
|
||||||
updatedTemplate, err := mgr.updateTemplate(ctx, name, updateTemplateOptions{
|
var opts updateTemplateOptions
|
||||||
description: *body.Description,
|
if body.Name != nil {
|
||||||
})
|
opts.name = *body.Name
|
||||||
|
}
|
||||||
|
if body.Description != nil {
|
||||||
|
opts.description = *body.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedTemplate, err := mgr.updateTemplate(ctx, name, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errTemplateNotFound) {
|
if errors.Is(err, errTemplateNotFound) {
|
||||||
return echo.NewHTTPError(http.StatusNotFound)
|
return echo.NewHTTPError(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, errTemplateExists) {
|
||||||
|
return echo.NewHTTPError(http.StatusConflict)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,7 +20,7 @@ type template struct {
|
|||||||
IsBuilt bool `json:"isBuilt"`
|
IsBuilt bool `json:"isBuilt"`
|
||||||
|
|
||||||
Files []*templateFile `bun:"rel:has-many,join:id=template_id" json:"-"`
|
Files []*templateFile `bun:"rel:has-many,join:id=template_id" json:"-"`
|
||||||
FileMap map[string]*templateFile `bun:"-" json:"files"`
|
FileMap map[string]*templateFile `bun:"-" json:"files,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type templateFile struct {
|
type templateFile struct {
|
||||||
|
@@ -30,7 +30,12 @@ type createTemplateOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type updateTemplateOptions struct {
|
type updateTemplateOptions struct {
|
||||||
tx *bun.Tx
|
tx *bun.Tx
|
||||||
|
|
||||||
|
// name is the new name for the template
|
||||||
|
name string
|
||||||
|
|
||||||
|
// description is the new description for the template
|
||||||
description string
|
description string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,10 +188,29 @@ func (mgr *templateManager) updateTemplate(ctx context.Context, name string, opt
|
|||||||
tx = &_tx
|
tx = &_tx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.name != "" {
|
||||||
|
exists, err := tx.NewSelect().
|
||||||
|
Table("templates").
|
||||||
|
Where("name = ?", opts.name).
|
||||||
|
Exists(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil, errTemplateExists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var template template
|
var template template
|
||||||
err := tx.NewUpdate().Model(&template).
|
q := tx.NewUpdate().Model(&template).Where("name = ?", name)
|
||||||
Where("Name = ?", name).
|
if opts.name != "" {
|
||||||
Set("description = ?", opts.description).
|
q = q.Set("name = ?", opts.name)
|
||||||
|
}
|
||||||
|
if opts.description != "" {
|
||||||
|
q = q.Set("description = ?", opts.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := q.
|
||||||
Returning("*").
|
Returning("*").
|
||||||
Scan(ctx)
|
Scan(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -11,7 +11,6 @@ const API_ERROR_WORKSPACE_EXISTS = "WORKSPACE_EXISTS";
|
|||||||
type ApiError =
|
type ApiError =
|
||||||
| { type: "NOT_FOUND" }
|
| { type: "NOT_FOUND" }
|
||||||
| { type: "NETWORK" }
|
| { type: "NETWORK" }
|
||||||
| { type: "BAD_REQUEST" }
|
|
||||||
| { type: "CONFLICT" }
|
| { type: "CONFLICT" }
|
||||||
| { type: "INTERNAL" }
|
| { type: "INTERNAL" }
|
||||||
| { type: "BAD_REQUEST"; details: ApiErrorDetails };
|
| { type: "BAD_REQUEST"; details: ApiErrorDetails };
|
||||||
|
@@ -15,6 +15,10 @@ interface OkStatus {
|
|||||||
type: "ok";
|
type: "ok";
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryStatus = IdleStatus | LoadingStatus | ErrorStatus | OkStatus;
|
type QueryStatus<TErr = unknown> =
|
||||||
|
| IdleStatus
|
||||||
|
| LoadingStatus
|
||||||
|
| ErrorStatus<TErr>
|
||||||
|
| OkStatus;
|
||||||
|
|
||||||
export type { QueryStatus, IdleStatus, LoadingStatus, ErrorStatus, OkStatus };
|
export type { QueryStatus, IdleStatus, LoadingStatus, ErrorStatus, OkStatus };
|
||||||
|
@@ -8,6 +8,7 @@ import type {
|
|||||||
TemplateImage,
|
TemplateImage,
|
||||||
TemplateMeta,
|
TemplateMeta,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { QueryStatus } from "@/lib/query";
|
||||||
|
|
||||||
function useTemplates() {
|
function useTemplates() {
|
||||||
return useSWR(
|
return useSWR(
|
||||||
@@ -93,6 +94,64 @@ function useCreateTemplate() {
|
|||||||
return { createTemplate, isCreatingTemplate: isCreating, error };
|
return { createTemplate, isCreatingTemplate: isCreating, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useUpdateTemplateMetadata() {
|
||||||
|
const [status, setStatus] = useState<QueryStatus<ApiError>>({ type: "idle" });
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
|
const updateTemplateMetadata = useCallback(
|
||||||
|
async ({
|
||||||
|
currentName,
|
||||||
|
newName,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
currentName: string;
|
||||||
|
newName?: string;
|
||||||
|
description?: string;
|
||||||
|
}): Promise<TemplateMeta | null> => {
|
||||||
|
setStatus({ type: "loading" });
|
||||||
|
try {
|
||||||
|
const body: Record<string, string> = {};
|
||||||
|
if (newName && newName !== currentName) {
|
||||||
|
body.name = newName;
|
||||||
|
}
|
||||||
|
if (description !== undefined) {
|
||||||
|
body.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await mutate(
|
||||||
|
`/templates/${currentName}`,
|
||||||
|
fetchApi(`/templates/${currentName}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}).then((res) =>
|
||||||
|
promiseOrThrow<TemplateMeta, ApiError>(res.json(), () => ({
|
||||||
|
type: "INTERNAL",
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
populateCache: (newTemplate, currentTemplate) => ({
|
||||||
|
...currentTemplate,
|
||||||
|
...newTemplate,
|
||||||
|
}),
|
||||||
|
revalidate: false,
|
||||||
|
throwOnError: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setStatus({ type: "ok" });
|
||||||
|
|
||||||
|
return result ?? null;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setStatus({ type: "error", error: err as ApiError });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mutate],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { updateTemplateMetadata, status };
|
||||||
|
}
|
||||||
|
|
||||||
function useTemplateFile(templateName: string, filePath: string) {
|
function useTemplateFile(templateName: string, filePath: string) {
|
||||||
return useSWR<string, ApiError>(
|
return useSWR<string, ApiError>(
|
||||||
filePath ? ["/templates", templateName, filePath] : null,
|
filePath ? ["/templates", templateName, filePath] : null,
|
||||||
@@ -188,6 +247,7 @@ export {
|
|||||||
useTemplate,
|
useTemplate,
|
||||||
useTemplateFile,
|
useTemplateFile,
|
||||||
useCreateTemplate,
|
useCreateTemplate,
|
||||||
|
useUpdateTemplateMetadata,
|
||||||
useUpdateTemplateFile,
|
useUpdateTemplateFile,
|
||||||
buildTemplate,
|
buildTemplate,
|
||||||
useDeleteTemplate,
|
useDeleteTemplate,
|
||||||
|
87
web/src/templates/template-editor-sidebar.tsx
Normal file
87
web/src/templates/template-editor-sidebar.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
} from "@/components/ui/sidebar";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { ArrowLeft, Pencil } from "lucide-react";
|
||||||
|
import { useTemplateEditorStore } from "./template-editor-store";
|
||||||
|
import { TemplateMetadataDialog } from "./template-metadata-dialog";
|
||||||
|
|
||||||
|
function TemplateEditorSidebar() {
|
||||||
|
const templateName = useTemplateEditorStore((state) => state.template.name);
|
||||||
|
const templateDescription = useTemplateEditorStore(
|
||||||
|
(state) => state.template.description || "No description",
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Sidebar>
|
||||||
|
<SidebarHeader>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton asChild className="opacity-80">
|
||||||
|
<Link to="/templates" className="text-xs">
|
||||||
|
<ArrowLeft /> All templates
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex flex-col px-2">
|
||||||
|
<p className="font-semibold">{templateName}</p>
|
||||||
|
<p className="text-xs opacity-80">{templateDescription}</p>
|
||||||
|
</div>
|
||||||
|
<TemplateNameDescriptionEditButton />
|
||||||
|
</div>
|
||||||
|
</SidebarHeader>
|
||||||
|
<SidebarContent>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>Files</SidebarGroupLabel>
|
||||||
|
<EditorSidebarFileTree />
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditorSidebarFileTree() {
|
||||||
|
const template = useTemplateEditorStore((state) => state.template);
|
||||||
|
const currentFilePath = useTemplateEditorStore(
|
||||||
|
(state) => state.currentFilePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarMenu>
|
||||||
|
{Object.values(template.files).map((file) => (
|
||||||
|
<SidebarMenuItem key={file.path}>
|
||||||
|
<SidebarMenuButton isActive={currentFilePath === file.path} asChild>
|
||||||
|
<Link to={`/templates/${template.name}/${file.path}`}>
|
||||||
|
{file.path}
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
</SidebarMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TemplateNameDescriptionEditButton() {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<Pencil />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<TemplateMetadataDialog />
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TemplateEditorSidebar };
|
@@ -2,7 +2,7 @@ import { type ApiErrorDetails, isApiErrorResponse } from "@/api";
|
|||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
import { createStore, useStore } from "zustand";
|
import { createStore, useStore } from "zustand";
|
||||||
import { buildTemplate } from "./api";
|
import { buildTemplate } from "./api";
|
||||||
import type { Template } from "./types";
|
import type { Template, TemplateMeta } from "./types";
|
||||||
|
|
||||||
interface TemplateEditorState {
|
interface TemplateEditorState {
|
||||||
template: Template;
|
template: Template;
|
||||||
@@ -25,6 +25,8 @@ interface TemplateEditorState {
|
|||||||
toggleBuildOutput: () => void;
|
toggleBuildOutput: () => void;
|
||||||
|
|
||||||
setIsVimModeEnabled: (enabled: boolean) => void;
|
setIsVimModeEnabled: (enabled: boolean) => void;
|
||||||
|
|
||||||
|
updateTemplateMetadata: (templateMetadata: TemplateMeta) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TemplateEditorStore = ReturnType<typeof createTemplateEditorStore>;
|
type TemplateEditorStore = ReturnType<typeof createTemplateEditorStore>;
|
||||||
@@ -86,6 +88,12 @@ function createTemplateEditorStore({
|
|||||||
|
|
||||||
setIsVimModeEnabled: (enabled: boolean) =>
|
setIsVimModeEnabled: (enabled: boolean) =>
|
||||||
set({ isVimModeEnabled: enabled }),
|
set({ isVimModeEnabled: enabled }),
|
||||||
|
|
||||||
|
updateTemplateMetadata: (templateMetadata) =>
|
||||||
|
set((state) => ({
|
||||||
|
...state,
|
||||||
|
template: { ...state.template, ...templateMetadata },
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,26 +1,17 @@
|
|||||||
import { API_ERROR_BAD_TEMPLATE } from "@/api";
|
import { API_ERROR_BAD_TEMPLATE } from "@/api";
|
||||||
import { CodeMirrorEditor } from "@/components/codemirror-editor";
|
import { CodeMirrorEditor } from "@/components/codemirror-editor";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import {
|
import { SidebarProvider } from "@/components/ui/sidebar.tsx";
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarProvider,
|
|
||||||
} from "@/components/ui/sidebar.tsx";
|
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Link, useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
import { ArrowLeft, ChevronDown, ChevronUp, Loader2 } from "lucide-react";
|
import { ChevronDown, ChevronUp, Loader2 } from "lucide-react";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useStore } from "zustand";
|
import { useStore } from "zustand";
|
||||||
import { useTemplate, useTemplateFile, useUpdateTemplateFile } from "./api";
|
import { useTemplate, useTemplateFile, useUpdateTemplateFile } from "./api";
|
||||||
import { templateEditorRoute } from "./routes";
|
import { templateEditorRoute } from "./routes";
|
||||||
|
import { TemplateEditorSidebar } from "./template-editor-sidebar";
|
||||||
import {
|
import {
|
||||||
type TemplateEditorStore,
|
type TemplateEditorStore,
|
||||||
TemplateEditorStoreContext,
|
TemplateEditorStoreContext,
|
||||||
@@ -111,7 +102,7 @@ function _TemplateEditor({
|
|||||||
return (
|
return (
|
||||||
<TemplateEditorStoreContext.Provider value={store.current}>
|
<TemplateEditorStoreContext.Provider value={store.current}>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<EditorSidebar />
|
<TemplateEditorSidebar />
|
||||||
<div className="flex flex-col w-full min-w-0">
|
<div className="flex flex-col w-full min-w-0">
|
||||||
<TemplateEditorTopBar />
|
<TemplateEditorTopBar />
|
||||||
<main className="w-full h-full flex flex-col">
|
<main className="w-full h-full flex flex-col">
|
||||||
@@ -216,53 +207,6 @@ function Editor() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorSidebar() {
|
|
||||||
const templateName = useTemplateEditorStore((state) => state.template.name);
|
|
||||||
return (
|
|
||||||
<Sidebar>
|
|
||||||
<SidebarHeader>
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton asChild className="opacity-80">
|
|
||||||
<Link to="/templates" className="text-xs">
|
|
||||||
<ArrowLeft /> All templates
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
<p className="px-2 font-semibold">{templateName}</p>
|
|
||||||
</SidebarHeader>
|
|
||||||
<SidebarContent>
|
|
||||||
<SidebarGroup>
|
|
||||||
<SidebarGroupLabel>Files</SidebarGroupLabel>
|
|
||||||
<EditorSidebarFileTree />
|
|
||||||
</SidebarGroup>
|
|
||||||
</SidebarContent>
|
|
||||||
</Sidebar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditorSidebarFileTree() {
|
|
||||||
const template = useTemplateEditorStore((state) => state.template);
|
|
||||||
const currentFilePath = useTemplateEditorStore(
|
|
||||||
(state) => state.currentFilePath,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarMenu>
|
|
||||||
{Object.values(template.files).map((file) => (
|
|
||||||
<SidebarMenuItem key={file.path}>
|
|
||||||
<SidebarMenuButton isActive={currentFilePath === file.path} asChild>
|
|
||||||
<Link to={`/templates/${template.name}/${file.path}`}>
|
|
||||||
{file.path}
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TemplateBuildOutputPanel() {
|
function TemplateBuildOutputPanel() {
|
||||||
const isBuildOutputVisible = useTemplateEditorStore(
|
const isBuildOutputVisible = useTemplateEditorStore(
|
||||||
(state) => state.isBuildOutputVisible,
|
(state) => state.isBuildOutputVisible,
|
||||||
|
161
web/src/templates/template-metadata-dialog.tsx
Normal file
161
web/src/templates/template-metadata-dialog.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { superstructResolver } from "@hookform/resolvers/superstruct";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { type Infer, object, optional, pattern, string } from "superstruct";
|
||||||
|
import { useUpdateTemplateMetadata } from "./api";
|
||||||
|
import { useTemplateEditorStore } from "./template-editor-store";
|
||||||
|
|
||||||
|
const TemplateMetadataFormSchema = object({
|
||||||
|
name: pattern(string(), /^[\w-]+$/),
|
||||||
|
description: optional(string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
function TemplateMetadataDialog() {
|
||||||
|
return (
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit metadata</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<TemplateMetadataForm />
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TemplateMetadataForm() {
|
||||||
|
const templateName = useTemplateEditorStore((state) => state.template.name);
|
||||||
|
const templateDescription = useTemplateEditorStore(
|
||||||
|
(state) => state.template.description,
|
||||||
|
);
|
||||||
|
const updateTemplateMetadataInStore = useTemplateEditorStore(
|
||||||
|
(state) => state.updateTemplateMetadata,
|
||||||
|
);
|
||||||
|
const { updateTemplateMetadata, status } = useUpdateTemplateMetadata();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const isUpdating = status.type === "loading";
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: superstructResolver(TemplateMetadataFormSchema),
|
||||||
|
disabled: isUpdating,
|
||||||
|
defaultValues: {
|
||||||
|
name: templateName,
|
||||||
|
description: templateDescription,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switch (status.type) {
|
||||||
|
case "error":
|
||||||
|
switch (status.error.type) {
|
||||||
|
case "CONFLICT":
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "This name is already in use.",
|
||||||
|
description: "Please choose another name.",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "NETWORK":
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to update template",
|
||||||
|
description: "Network error",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to update template",
|
||||||
|
description: "Unknown error",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ok":
|
||||||
|
toast({
|
||||||
|
title: "Template updated!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [status, toast]);
|
||||||
|
|
||||||
|
async function onSubmit(values: Infer<typeof TemplateMetadataFormSchema>) {
|
||||||
|
const updated = await updateTemplateMetadata({
|
||||||
|
currentName: templateName,
|
||||||
|
newName: values.name,
|
||||||
|
description: values.description,
|
||||||
|
});
|
||||||
|
if (updated) {
|
||||||
|
console.log(updated);
|
||||||
|
updateTemplateMetadataInStore(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Template name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Must only contain alphanumeric characters and "-".
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Must only contain alphanumeric characters and "-".
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button disabled={isUpdating} type="submit">
|
||||||
|
{isUpdating ? <LoadingSpinner /> : null}
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TemplateMetadataDialog };
|
Reference in New Issue
Block a user