diff --git a/packages/app/src/actions/data/templates/applyTemplate.ts b/packages/app/src/actions/data/templates/applyTemplate.ts index c3b447b9..555464db 100644 --- a/packages/app/src/actions/data/templates/applyTemplate.ts +++ b/packages/app/src/actions/data/templates/applyTemplate.ts @@ -11,6 +11,11 @@ import { PlotDefinitionWithoutSource, } from "@common/db/schema/template"; import { plotDefinitionSchema } from "@common/db/schema/plot"; +import { + actionError, + actionResult, + runActionServer, +} from "@/lib/actions/utils"; type ApplyTemplateParams = { projectId: string; @@ -29,61 +34,63 @@ export async function applyTemplateAction({ plotDefinition, folderId, }: ApplyTemplateParams) { - const user = await getServerUser(); - const userProject = await getProjectForUser(user, projectId); + return runActionServer(async () => { + const user = await getServerUser(); + const userProject = await getProjectForUser(user, projectId); - if (!userProject) { - throw new Error("Project not found"); - } + if (!userProject) { + return actionError("Project not found"); + } - // Create query - const newQuery = await createQuery({ - name: queryName, - projectId, - folderId: folderId || null, - definition: Object.fromEntries( - Object.entries(queryDefinition).map(([key, value]) => [ - key, - { - ...value, - filters: {}, - }, - ]), - ), - }); + // Create query + const newQuery = await createQuery({ + name: queryName, + projectId, + folderId: folderId || null, + definition: Object.fromEntries( + Object.entries(queryDefinition).map(([key, value]) => [ + key, + { + ...value, + filters: {}, + }, + ]), + ), + }); - revalidateQueryCache(newQuery.id, projectId); + revalidateQueryCache(newQuery.id, projectId); - let newPlot = null; - let url = `/projects/${projectId}/data/queries/${newQuery.id}`; + let newPlot = null; + let url = `/projects/${projectId}/data/queries/${newQuery.id}`; - // Create plot if provided - if (plotName && plotDefinition) { - const plotDefinitionWithSource = plotDefinitionSchema.parse({ - ...plotDefinition, - dataSource: { - isQuery: true, - queryId: newQuery.id, - }, - }); + // Create plot if provided + if (plotName && plotDefinition) { + const plotDefinitionWithSource = plotDefinitionSchema.parse({ + ...plotDefinition, + dataSource: { + isQuery: true, + queryId: newQuery.id, + }, + }); - newPlot = await createPlot({ - name: plotName, - projectId, - folderId: folderId || null, - definition: plotDefinitionWithSource, - sourceQueryId: newQuery.id, - }); + newPlot = await createPlot({ + name: plotName, + projectId, + folderId: folderId || null, + definition: plotDefinitionWithSource, + sourceQueryId: newQuery.id, + }); - url = `/projects/${projectId}/data/plots/${newPlot.id}`; + url = `/projects/${projectId}/data/plots/${newPlot.id}`; - revalidateTag(`project-${projectId}-plots`); - revalidateTag(`project-${projectId}-data`); - } + revalidateTag(`project-${projectId}-plots`); + revalidateTag(`project-${projectId}-data`); + } - return { - query: newQuery, - plot: newPlot, - url, - }; + return actionResult({ + query: newQuery, + plot: newPlot, + url, + }); + }); } diff --git a/packages/app/src/actions/data/templates/createTemplate.ts b/packages/app/src/actions/data/templates/createTemplate.ts index 774ffef9..ba4ae274 100644 --- a/packages/app/src/actions/data/templates/createTemplate.ts +++ b/packages/app/src/actions/data/templates/createTemplate.ts @@ -11,6 +11,11 @@ import { QueryWithoutFilters, PlotDefinitionWithoutSource, } from "@common/db/schema/template"; +import { + runActionServer, + actionError, + actionResult, +} from "@/lib/actions/utils"; interface CreateTemplateParams { name: string; @@ -31,64 +36,126 @@ export async function createTemplateAction({ projectId, tagIds = [], }: CreateTemplateParams) { - const user = await getServerUser(); + return runActionServer(async () => { + const user = await getServerUser(); - if (!user) { - throw new Error("User not authenticated"); - } + if (!user) { + return actionError("User not authenticated"); + } - let queryDefinition: QueryWithoutFilters | null = null; - let plotDefinition: PlotDefinitionWithoutSource | null = null; + let queryDefinition: QueryWithoutFilters | null = null; + let plotDefinition: PlotDefinitionWithoutSource | null = null; - // Get query definition if provided - if (queryId) { - const query = await getQueryForProject(projectId, queryId); - if (!query || !query.definition) { - throw new Error("query not found or has no definition"); - } + // Get query definition if provided + if (queryId) { + const query = await getQueryForProject(projectId, queryId); + if (!query || !query.definition) { + return actionError("Query not found or has no definition"); + } - // Remove filters from the definition for the template - queryDefinition = Object.fromEntries( - Object.entries(query.definition).map(([tableName, tableDef]) => [ - tableName, - { - ...tableDef, - filters: undefined, // Remove filters - }, - ]), - ) as Record; - } - - // Get plot definition if provided - if (plotId) { - const plot = await getPlotForProject(projectId, plotId); - if (!plot || !plot.definition) { - throw new Error("Plot not found or has no definition"); + // Remove filters from the definition for the template + queryDefinition = Object.fromEntries( + Object.entries(query.definition).map(([tableName, tableDef]) => [ + tableName, + { + ...tableDef, + filters: undefined, // Remove filters + }, + ]), + ) as Record; } - // Remove data source from the definition for the template - // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars - const { dataSource, ...plotDefWithoutSource } = plot.definition; + // Get plot definition if provided + if (plotId) { + const plot = await getPlotForProject(projectId, plotId); + if (!plot || !plot.definition) { + return actionError("Plot not found or has no definition"); + } - plotDefinition = plotDefWithoutSource; - } + // If this is a plot template and we have a queryId (from plot.sourceQueryId), + // make sure we have the query definition + if (!queryDefinition && queryId) { + const query = await getQueryForProject(projectId, queryId); + if (!query || !query.definition) { + return actionError("Query not found or has no definition"); + } - // Create the template - const newTemplate = await createTemplate({ - name, - ownerId: user.id, - queryDefinition: queryDefinition || ({} as QueryWithoutFilters), - plotDefinition: plotDefinition || null, - scope, - }); + // Remove filters from the definition for the template + queryDefinition = Object.fromEntries( + Object.entries(query.definition).map(([tableName, tableDef]) => [ + tableName, + { + ...tableDef, + filters: undefined, // Remove filters + }, + ]), + ) as Record; + } + + // If we still don't have a query definition, try to get it from the plot's definition.dataSource + if ( + !queryDefinition && + plot.definition.dataSource?.isQuery && + plot.definition.dataSource.queryId + ) { + const query = await getQueryForProject( + projectId, + plot.definition.dataSource.queryId, + ); + if (!query || !query.definition) { + return actionError("Query not found or has no definition"); + } + + // Remove filters from the definition for the template + queryDefinition = Object.fromEntries( + Object.entries(query.definition).map(([tableName, tableDef]) => [ + tableName, + { + ...tableDef, + filters: undefined, // Remove filters + }, + ]), + ) as Record; + } + + // Ensure we have a query definition for plot templates + if (!queryDefinition) { + return actionError( + "Plot templates must have an associated query definition", + ); + } - // Create template tags if provided - if (tagIds.length > 0) { - await createTemplateTags(newTemplate.id, tagIds); - } + // Only require query definition for plots that use queries + if (plot.definition.dataSource?.isQuery === true && !queryDefinition) { + return actionError( + "Plot templates that use queries must have an associated query definition", + ); + } - // Revalidate user's templates cache - revalidateTag(`user-${user.id}-templates`); + // Remove data source from the definition for the template + // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars + const { dataSource, ...plotDefWithoutSource } = plot.definition; - return newTemplate; + plotDefinition = plotDefWithoutSource; + } + + // Create the template + const newTemplate = await createTemplate({ + name, + ownerId: user.id, + queryDefinition: queryDefinition || ({} as QueryWithoutFilters), + plotDefinition: plotDefinition || null, + scope, + }); + + // Create template tags if provided + if (tagIds.length > 0) { + await createTemplateTags(newTemplate.id, tagIds); + } + + // Revalidate user's templates cache + revalidateTag(`user-${user.id}-templates`); + + return actionResult(newTemplate); + }); } diff --git a/packages/app/src/actions/data/templates/readTemplates.ts b/packages/app/src/actions/data/templates/readTemplates.ts deleted file mode 100644 index ac55ec85..00000000 --- a/packages/app/src/actions/data/templates/readTemplates.ts +++ /dev/null @@ -1,9 +0,0 @@ -"use server"; - -import { getServerUser } from "@/lib/auth"; -import { readTemplatesByUserId } from "@/db/crud/template"; - -export async function readTemplatesAction() { - const user = await getServerUser(); - return readTemplatesByUserId(user.id); -} diff --git a/packages/app/src/actions/data/templates/searchTags.ts b/packages/app/src/actions/data/templates/searchTags.ts new file mode 100644 index 00000000..9647a266 --- /dev/null +++ b/packages/app/src/actions/data/templates/searchTags.ts @@ -0,0 +1,27 @@ +"use server"; + +import { getServerUser } from "@/lib/auth"; +import { searchGlobalTagsWithUsageCounts } from "@/db/crud/tag"; + +interface SearchTagsParams { + searchTerm: string; + limit?: number; +} + +export async function searchTagsAction({ + searchTerm, + limit = 20, +}: SearchTagsParams) { + const user = await getServerUser(); + + if (!user) { + throw new Error("User not authenticated"); + } + + if (!searchTerm.trim()) { + return []; + } + + const tags = await searchGlobalTagsWithUsageCounts(searchTerm.trim(), limit); + return tags; +} diff --git a/packages/app/src/actions/data/templates/updateTemplate.ts b/packages/app/src/actions/data/templates/updateTemplate.ts index 325a0a4d..b7dc8511 100644 --- a/packages/app/src/actions/data/templates/updateTemplate.ts +++ b/packages/app/src/actions/data/templates/updateTemplate.ts @@ -9,53 +9,62 @@ import { import { db } from "@/db"; import { templateTag } from "@common/db/schema/template"; import { eq } from "drizzle-orm"; -import { revalidateTag } from "next/cache"; +import { revalidatePath } from "next/cache"; +import { + actionError, + actionResult, + runActionServer, +} from "@/lib/actions/utils"; -interface UpdateTemplateParams { +type UpdateTemplateParams = { templateId: string; name: string; + description?: string; scope: "public" | "private"; tagIds: string[]; -} +}; export async function updateTemplateAction({ templateId, name, + description, scope, tagIds, }: UpdateTemplateParams) { - const user = await getServerUser(); - - if (!user) { - throw new Error("User not authenticated"); - } - - // Verify the user owns this template - const existingTemplate = await readTemplateForUser(user.id, templateId); - - if (!existingTemplate) { - throw new Error( - "Template not found or you don't have permission to edit it", - ); - } - - // Update the template - const updatedTemplate = await updateTemplate(templateId, { - name, - scope, - updatedAt: new Date(), - }); + return runActionServer(async () => { + const user = await getServerUser(); + + if (!user) { + return actionError("User not authenticated"); + } - // Update tags - first remove all existing tags - await db.delete(templateTag).where(eq(templateTag.templateId, templateId)); + // Verify the user owns this template + const existingTemplate = await readTemplateForUser(user.id, templateId); - // Add new tags if provided - if (tagIds.length > 0) { - await createTemplateTags(templateId, tagIds); - } + if (!existingTemplate) { + return actionError( + "Template not found or you don't have permission to edit it", + ); + } - // Revalidate user's templates cache - revalidateTag(`user-${user.id}-templates`); + // Update the template + const updatedTemplate = await updateTemplate(templateId, { + name, + description, + scope, + }); - return updatedTemplate; + // Update tags - first remove all existing tags + await db.delete(templateTag).where(eq(templateTag.templateId, templateId)); + + // Add new tags if provided + if (tagIds.length > 0) { + await createTemplateTags(templateId, tagIds); + } + + // Revalidate user's templates cache + revalidatePath("/templates"); + + return actionResult(updatedTemplate); + }); } diff --git a/packages/app/src/app/(app)/projects/[projectId]/data/queries/[queryId]/layout.tsx b/packages/app/src/app/(app)/projects/[projectId]/data/queries/[queryId]/layout.tsx index 965ba30e..c5379ed8 100644 --- a/packages/app/src/app/(app)/projects/[projectId]/data/queries/[queryId]/layout.tsx +++ b/packages/app/src/app/(app)/projects/[projectId]/data/queries/[queryId]/layout.tsx @@ -14,10 +14,9 @@ import { getQueryForProject } from "@/lib/dal/queries"; type Props = { params: { projectId: string; queryId: string }; children: React.ReactNode; - modal: React.ReactNode; }; -export default async function Layout({ params, children, modal }: Props) { +export default async function Layout({ params, children }: Props) { const projectId = parseStringParam(params.projectId); const user = await getServerUser(); await getProjectForUser(user, projectId); @@ -59,7 +58,6 @@ export default async function Layout({ params, children, modal }: Props) { {children} - {modal} ); } diff --git a/packages/app/src/app/(app)/templates/page.tsx b/packages/app/src/app/(app)/templates/page.tsx index a465cabe..92810d65 100644 --- a/packages/app/src/app/(app)/templates/page.tsx +++ b/packages/app/src/app/(app)/templates/page.tsx @@ -1,7 +1,10 @@ import { getServerUser } from "@/lib/auth"; import { PageFrame } from "@/components/common/page-frame"; -import { TemplatesList } from "@/components/templates/templates-list"; +import { TemplatesList } from "@/components/data/templates/templates-list"; import { FileText } from "lucide-react"; +import { readTagsWithUsageCountsByUserId } from "@/db/crud/tag"; +import { readTemplatesWithSearchAndFilters } from "@/db/crud/template"; +import { TemplateFilters } from "@/actions/data/templates/readTemplatesWithFilters"; type Props = { searchParams: { @@ -11,6 +14,8 @@ type Props = { tags?: string; page?: string; pageSize?: string; + template?: string; + editing?: string; }; }; @@ -23,8 +28,10 @@ export default async function Page({ searchParams }: Props) { const type = (searchParams.type as "all" | "query" | "plot") || "all"; const scope = (searchParams.scope as "all" | "public" | "private") || "all"; const tagIds = searchParams.tags ? searchParams.tags.split(",") : []; + const selectedTemplateId = searchParams.template || null; + const isEditing = searchParams.editing === "true"; - const initialFilters = { + const initialFilters: TemplateFilters = { search: searchQuery, type, scope, @@ -33,6 +40,13 @@ export default async function Page({ searchParams }: Props) { pageSize, }; + const { templates, pagination } = await readTemplatesWithSearchAndFilters( + user.id, + initialFilters, + ); + + const allTags = await readTagsWithUsageCountsByUserId(user.id); + return ( } classNames={{ container: "h-full" }} > - + ); } diff --git a/packages/app/src/components/data/data-sidebar/folder-tree.tsx b/packages/app/src/components/data/data-sidebar/folder-tree.tsx index c3c3cfa0..faedc3b6 100644 --- a/packages/app/src/components/data/data-sidebar/folder-tree.tsx +++ b/packages/app/src/components/data/data-sidebar/folder-tree.tsx @@ -413,15 +413,6 @@ export function FolderTree({ onItemDelete, onItemDuplicate, }: FolderTreeProps) { - console.log( - "FolderTree received folders:", - folders.map((f) => ({ - id: f.id, - name: f.name, - parentFolderId: f.parentFolderId, - })), - ); - const [expandedFolders, setExpandedFolders] = useState>( new Set(), ); @@ -500,32 +491,20 @@ export function FolderTree({ const [draggedType, draggedId] = (active.id as string).split(":"); const [droppedType, droppedId] = (over.id as string).split(":"); - console.log("Drag end:", { - draggedType, - draggedId, - droppedType, - droppedId, - }); - // Only allow dropping on folders if (droppedType !== "folder") return; const targetFolderId = droppedId === "root" ? null : droppedId; const draggedItemId = draggedId; - console.log("Moving item:", { draggedItemId, targetFolderId }); - try { if (draggedType === "folder") { // Prevent dropping a folder into itself or its descendants if (targetFolderId === draggedItemId) { - console.log("Cannot drop folder into itself"); return; } - console.log("Moving folder:", { draggedItemId, targetFolderId }); await moveFolderAction(projectId, draggedItemId, targetFolderId); - console.log("Folder moved successfully"); } else if (draggedType === "item") { if (itemType === "query") { await moveQueryAction(projectId, draggedItemId, targetFolderId); @@ -534,7 +513,6 @@ export function FolderTree({ } } - console.log("Refreshing page data"); router.refresh(); } catch (error) { console.error("Error moving item:", error); diff --git a/packages/app/src/components/data/query/query-glide.tsx b/packages/app/src/components/data/query/query-glide.tsx index d9b051b5..37839e91 100644 --- a/packages/app/src/components/data/query/query-glide.tsx +++ b/packages/app/src/components/data/query/query-glide.tsx @@ -164,7 +164,6 @@ export function QueryGlide({ const zoneIds = zones .filter((zone) => zone.isSelected) .map((zone) => zone.id); - console.log(zoneIds); // Use the virtualized query data hook const { rowCount, getRawCellData, isLoading, setVisibleRegion } = diff --git a/packages/app/src/components/data/templates/create-template-form.tsx b/packages/app/src/components/data/templates/create-template-form.tsx new file mode 100644 index 00000000..3926b0e1 --- /dev/null +++ b/packages/app/src/components/data/templates/create-template-form.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { TemplateEditForm } from "./template-edit-form"; +import { Tag } from "@common/db/schema/template"; +import { TagWithUsageCount } from "@/db/crud/tag"; + +type CreateTemplateFormProps = { + projectId: string; + plotId?: string; + queryId?: string; + itemName: string; + itemType: "plot" | "query"; + allTags: (Tag | TagWithUsageCount)[]; + onSuccess?: () => void; + onCancel?: () => void; +}; + +export function CreateTemplateForm({ + projectId, + plotId, + queryId, + itemName, + itemType, + allTags, + onSuccess, + onCancel = () => {}, +}: CreateTemplateFormProps) { + return ( + + ); +} diff --git a/packages/app/src/components/data/templates/enhanced-tag-autocomplete.tsx b/packages/app/src/components/data/templates/enhanced-tag-autocomplete.tsx new file mode 100644 index 00000000..b387abaa --- /dev/null +++ b/packages/app/src/components/data/templates/enhanced-tag-autocomplete.tsx @@ -0,0 +1,278 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Check, ChevronsUpDown, Plus, Loader2, Search } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Badge } from "@/components/ui/badge"; +import { X } from "lucide-react"; +import { cn } from "@/utils/styles"; +import { Tag } from "@common/db/schema/template"; +import { TagWithUsageCount } from "@/db/crud/tag"; +import { createTagAction } from "@/actions/data/templates/createTag"; +import { searchTagsAction } from "@/actions/data/templates/searchTags"; +import { toast } from "sonner"; +import { useRouter } from "next/navigation"; + +type EnhancedTagAutocompleteProps = { + selectedTags: Tag[]; + onTagsChange: (_tags: Tag[]) => void; + onTagCreated?: (_tag: Tag) => void; + placeholder?: string; + disabled?: boolean; + className?: string; +}; + +export function EnhancedTagAutocomplete({ + selectedTags, + onTagsChange, + onTagCreated, + placeholder = "Search and select tags...", + disabled = false, + className, +}: EnhancedTagAutocompleteProps) { + const [open, setOpen] = useState(false); + const [searchValue, setSearchValue] = useState(""); + const [isSearching, setIsSearching] = useState(false); + const [isCreatingTag, setIsCreatingTag] = useState(false); + const [searchResults, setSearchResults] = useState([]); + const router = useRouter(); + + // Debounce search to avoid too many API calls + const [debouncedSearch, setDebouncedSearch] = useState(""); + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearch(searchValue); + }, 300); + + return () => clearTimeout(timer); + }, [searchValue]); + + // Search for tags when search value changes + useEffect(() => { + if (!debouncedSearch.trim()) { + setSearchResults([]); + return; + } + + const performSearch = async () => { + setIsSearching(true); + try { + const results = await searchTagsAction({ searchTerm: debouncedSearch }); + setSearchResults(results); + } catch (error) { + console.error("Failed to search tags:", error); + toast.error("Failed to search tags"); + } finally { + setIsSearching(false); + } + }; + + performSearch(); + }, [debouncedSearch]); + + // Filter out already selected tags from search results + const availableTags = searchResults.filter( + (tag) => !selectedTags.find((selected) => selected.id === tag.id), + ); + + const handleSelectTag = (tag: TagWithUsageCount) => { + onTagsChange([...selectedTags, tag]); + setSearchValue(""); + setOpen(false); + }; + + const handleRemoveTag = (tagId: string) => { + onTagsChange(selectedTags.filter((tag) => tag.id !== tagId)); + }; + + const handleCreateTag = async (tagName: string) => { + if (!tagName.trim()) { + toast.error("Tag name is required"); + return; + } + + setIsCreatingTag(true); + + try { + const newTag = await createTagAction({ + name: tagName.trim(), + }); + + // Add the new tag to the selection - use current selectedTags to avoid stale closure + const currentSelectedTags = [...selectedTags]; + const newSelectedTags = [...currentSelectedTags, newTag]; + + onTagsChange(newSelectedTags); + + // Trigger the onTagCreated callback if provided + if (onTagCreated) { + onTagCreated(newTag); + } + + // Reset form + setSearchValue(""); + setOpen(false); + + toast.success("Tag created successfully"); + + // Trigger a router refresh to update server data + router.refresh(); + } catch (error) { + console.error("Failed to create tag:", error); + toast.error("Failed to create tag"); + } finally { + setIsCreatingTag(false); + } + }; + + const displayValue = + selectedTags.length > 0 + ? `${selectedTags.length} tag${selectedTags.length === 1 ? "" : "s"} selected` + : placeholder; + + return ( +
+ {/* Public tag warning */} +
+ Note: Tag names are public and visible to all users. Do + not include sensitive information in tag names. +
+ + {/* Selected tags display */} + {selectedTags.length > 0 && ( +
+ {selectedTags.map((tag) => ( + + {tag.name} + + + ))} +
+ )} + + {/* Tag selector */} + + + + + + + + + {isSearching && ( +
+ + + Searching... + +
+ )} + + {!isSearching && searchValue && ( + <> + {/* Search results */} + {availableTags.length > 0 && ( + + {availableTags.map((tag) => ( + handleSelectTag(tag)} + className="flex items-center gap-2" + > + + {tag.name} + + ({tag.usageCount}) + + + ))} + + )} + + {/* Create new tag option */} + + handleCreateTag(searchValue)} + className="flex items-center gap-2 text-muted-foreground" + disabled={isCreatingTag} + > + {isCreatingTag ? ( + + ) : ( + + )} + Create "{searchValue}" + + + + )} + + {!isSearching && !searchValue && ( + +
+ +

+ Start typing to search for tags +

+
+
+ )} + + {!isSearching && searchValue && availableTags.length === 0 && ( + +
+

+ No tags found for "{searchValue}" +

+

+ You can create a new tag using the option above +

+
+
+ )} +
+
+
+
+
+ ); +} diff --git a/packages/app/src/components/data/templates/index.ts b/packages/app/src/components/data/templates/index.ts index 1c44616b..59cd3ca4 100644 --- a/packages/app/src/components/data/templates/index.ts +++ b/packages/app/src/components/data/templates/index.ts @@ -1,7 +1,7 @@ export { TemplatesModal } from "./templates-modal"; -export { SaveTemplateForm } from "./save-template-form"; -export { TagAutocomplete } from "./tag-autocomplete"; +export { CreateTemplateForm } from "./create-template-form"; +export { TemplateEditForm } from "./template-edit-form"; + export { TemplateCard } from "./template-card"; -export { TemplateApplicationModal } from "./template-application-modal"; export { SaveQueryTemplateModal } from "./save-query-template-modal"; export { SavePlotTemplateModal } from "./save-plot-template-modal"; diff --git a/packages/app/src/components/data/templates/save-plot-template-modal.tsx b/packages/app/src/components/data/templates/save-plot-template-modal.tsx index d30cf677..5f981783 100644 --- a/packages/app/src/components/data/templates/save-plot-template-modal.tsx +++ b/packages/app/src/components/data/templates/save-plot-template-modal.tsx @@ -10,7 +10,7 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Loader2 } from "lucide-react"; -import { SaveTemplateForm } from "./save-template-form"; +import { CreateTemplateForm } from "./create-template-form"; import { useSavePlotTemplate } from "@/hooks/use-save-plot-template"; type SavePlotTemplateModalProps = { @@ -89,9 +89,10 @@ export function SavePlotTemplateModal({ - - ; - -type SaveTemplateFormProps = { - projectId: string; - plotId?: string; - queryId?: string; - itemName: string; - itemType: "plot" | "query"; - allTags: (Tag | TagWithUsageCount)[]; - onSuccess: () => void; - onCancel: () => void; -}; - -export function SaveTemplateForm({ - projectId, - plotId, - queryId, - itemName, - itemType: _itemType, - allTags, - onSuccess, - onCancel, -}: SaveTemplateFormProps) { - const [isLoading, setIsLoading] = useState(false); - - const form = useForm({ - resolver: zodResolver(saveTemplateSchema), - defaultValues: { - name: `${itemName} template`, - description: "", - scope: "private", - tagIds: [], - }, - }); - - const selectedTagIds = form.watch("tagIds"); - const selectedTags = allTags.filter((tag) => selectedTagIds.includes(tag.id)); - - const handleSubmit = async (data: SaveTemplateFormData) => { - setIsLoading(true); - - try { - await createTemplateAction({ - name: data.name.trim(), - description: data.description?.trim() || undefined, - scope: data.scope, - plotId, - queryId, - projectId, - tagIds: data.tagIds, - }); - - toast.success("Template saved successfully"); - onSuccess(); - } catch (error) { - console.error("Failed to save template:", error); - toast.error("Failed to save template"); - } finally { - setIsLoading(false); - } - }; - - const handleTagsChange = (tags: Tag[]) => { - form.setValue( - "tagIds", - tags.map((tag) => tag.id), - ); - }; - - return ( -
- - ( - - Template name - - - - - - )} - /> - - ( - - Description (optional) - -