diff --git a/packages/core/src/db/schemas.ts b/packages/core/src/db/schemas.ts index be1a1ff2..39e9db19 100644 --- a/packages/core/src/db/schemas.ts +++ b/packages/core/src/db/schemas.ts @@ -998,7 +998,12 @@ export type RPDBIsValidResponse = z.infer; export const TemplateSchema = z.object({ metadata: z.object({ - id: z.string().min(1).max(100).optional().default(crypto.randomUUID()), + id: z + .string() + .min(1) + .max(100) + .optional() + .transform((val) => val ?? crypto.randomUUID()), name: z.string().min(1).max(100), // name of the template description: z.string().min(1).max(1000), // description of the template author: z.string().min(1).max(20), // author of the template diff --git a/packages/frontend/src/components/shared/config-templates-modal.tsx b/packages/frontend/src/components/shared/config-templates-modal.tsx index d30ad339..147710f6 100644 --- a/packages/frontend/src/components/shared/config-templates-modal.tsx +++ b/packages/frontend/src/components/shared/config-templates-modal.tsx @@ -25,6 +25,8 @@ import { import { PasswordInput } from '../ui/password-input'; import React from 'react'; import { z, ZodError } from 'zod'; +import { Tooltip } from '../ui/tooltip'; +import { cn } from '../ui/core/styling'; const formatZodError = (error: ZodError) => { console.log(JSON.stringify(error, null, 2)); @@ -33,7 +35,12 @@ const formatZodError = (error: ZodError) => { const TemplateSchema = z.object({ metadata: z.object({ - id: z.string().min(1).max(100).optional().default(crypto.randomUUID()), + id: z + .string() + .min(1) + .max(100) + .optional() + .transform((val) => val ?? crypto.randomUUID()), name: z.string().min(1).max(100), // name of the template description: z.string().min(1).max(1000), // description of the template author: z.string().min(1).max(20), // author of the template @@ -92,6 +99,7 @@ export function ConfigTemplatesModal({ const { status } = useStatus(); const [searchQuery, setSearchQuery] = useState(''); const [selectedCategory, setSelectedCategory] = useState('all'); + const [selectedSource, setSelectedSource] = useState('all'); const [isLoading, setIsLoading] = useState(false); const [templates, setTemplates] = useState([]); const [loadingTemplates, setLoadingTemplates] = useState(false); @@ -314,6 +322,8 @@ export function ConfigTemplatesModal({ ...Array.from(new Set(templates.map((t) => t.metadata.category))), ]; + const sources = ['all', 'builtin', 'custom', 'external']; + const filteredTemplates = templates.filter((template) => { const matchesSearch = template.metadata.name @@ -330,7 +340,10 @@ export function ConfigTemplatesModal({ selectedCategory === 'all' || template.metadata.category === selectedCategory; - return matchesSearch && matchesCategory; + const matchesSource = + selectedSource === 'all' || template.metadata.source === selectedSource; + + return matchesSearch && matchesCategory && matchesSource; }); const processImportedTemplate = (data: any) => { @@ -1065,30 +1078,95 @@ export function ConfigTemplatesModal({ // Render different steps const renderBrowse = () => ( <> - {/* Search and Filter */} -
-
- } - /> + {/* Search and Filters */} +
+ {/* Search Bar - Full Width */} + } + /> + + {/* Source Filters */} +
+ Source: +
+ {sources.map((source: string) => { + const sourceDescription = { + all: 'All sources', + builtin: 'Provided with AIOStreams', + custom: 'Added by the instance hoster', + external: 'Imported by you', + }; + const colorClasses = { + all: 'bg-gray-700/50 text-gray-300 hover:bg-gray-700', + builtin: + selectedSource === 'builtin' + ? 'bg-brand-500/20 text-brand-300 border border-brand-500/30' + : 'bg-brand-500/10 text-brand-400 hover:bg-brand-500/20 border border-brand-500/20', + custom: + selectedSource === 'custom' + ? 'bg-purple-500/20 text-purple-300 border border-purple-500/30' + : 'bg-purple-500/10 text-purple-400 hover:bg-purple-500/20 border border-purple-500/20', + external: + selectedSource === 'external' + ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30' + : 'bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20 border border-emerald-500/20', + }; + const tooltipColorClasses = { + all: 'bg-gray-800 text-white border-gray-700', + builtin: 'bg-brand-600 text-white border-brand-500', + custom: 'bg-purple-600 text-white border-purple-500', + external: 'bg-emerald-600 text-white border-emerald-500', + }; + return ( + setSelectedSource(source)} + className={`px-3 py-1 text-xs font-medium rounded transition-colors whitespace-nowrap flex-shrink-0 ${ + selectedSource === source && source === 'all' + ? 'bg-gray-600 text-white' + : colorClasses[source as keyof typeof colorClasses] + }`} + > + {source.charAt(0).toUpperCase() + source.slice(1)} + + } + > + {sourceDescription[source as keyof typeof sourceDescription]} + + ); + })} +
-
- {categories.map((category) => ( - - ))} + + {/* Category Filters - Scrollable */} +
+ Category: +
+ {categories.map((category) => ( + + ))} +
@@ -1134,7 +1212,7 @@ export function ConfigTemplatesModal({
{template.metadata.source === 'builtin' && ( - + Built-in )} @@ -1303,18 +1381,21 @@ export function ConfigTemplatesModal({
- {filteredTemplates.length} template - {filteredTemplates.length !== 1 ? 's' : ''} available + {templates.length} template + {templates.length !== 1 ? 's' : ''} available
- } - intent="primary-outline" - onClick={() => setShowImportModal(true)} - /> - + } + onClick={() => setShowImportModal(true)} + /> + } + > + Import Template +
@@ -1489,7 +1570,9 @@ export function ConfigTemplatesModal({ title="Templates" description="Browse and load pre-configured templates for your AIOStreams setup" > -
{renderBrowse()}
+
+ {renderBrowse()} +
{/* Service Selection Modal */} @@ -1561,18 +1644,6 @@ export function ConfigTemplatesModal({ > Import from File - -
- -