From a59023f75be3ef66308c5e996f2b8fa5e5765920 Mon Sep 17 00:00:00 2001 From: yessenia Date: Thu, 5 Feb 2026 16:47:56 +0800 Subject: [PATCH] feat: add templates marketplace functionality --- .../plugins/marketplace/list/list-wrapper.tsx | 39 +- .../marketplace/list/template-card.tsx | 92 +++++ .../marketplace/list/template-list.tsx | 129 +++++++ .../components/plugins/marketplace/query.ts | 33 +- .../components/plugins/marketplace/state.ts | 141 +++++++- .../components/plugins/marketplace/types.ts | 127 +++++++ .../components/plugins/marketplace/utils.ts | 105 ++++++ .../components/plugins/plugin-page/index.tsx | 2 +- ...equest-dropdown.tsx => nav-operations.tsx} | 49 ++- web/contract/marketplace.ts | 334 +++++++++++++++++- web/contract/router.ts | 50 ++- web/i18n/en-US/plugin.json | 2 + web/i18n/zh-Hans/plugin.json | 2 + 13 files changed, 1090 insertions(+), 15 deletions(-) create mode 100644 web/app/components/plugins/marketplace/list/template-card.tsx create mode 100644 web/app/components/plugins/marketplace/list/template-list.tsx rename web/app/components/plugins/plugin-page/{submit-request-dropdown.tsx => nav-operations.tsx} (57%) diff --git a/web/app/components/plugins/marketplace/list/list-wrapper.tsx b/web/app/components/plugins/marketplace/list/list-wrapper.tsx index 16278206b8..7a3e822563 100644 --- a/web/app/components/plugins/marketplace/list/list-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/list-wrapper.tsx @@ -11,6 +11,7 @@ import { PLUGIN_TYPE_SEARCH_MAP } from '../constants' import SortDropdown from '../sort-dropdown' import { useMarketplaceData } from '../state' import List from './index' +import TemplateList from './template-list' type ListWrapperProps = { showInstallButton?: boolean @@ -31,15 +32,49 @@ const ListWrapper = ({ const [activePluginType, handleActivePluginTypeChange] = useActivePluginType() const [searchScope, setSearchScope] = useState('all') + const marketplaceData = useMarketplaceData() + const { + creationType, + isLoading, + } = marketplaceData + + // Templates view + if (creationType === 'templates') { + const { templateCollections, templateCollectionTemplatesMap } = marketplaceData + return ( +
+ { + isLoading && ( +
+ +
+ ) + } + { + !isLoading && ( + + ) + } +
+ ) + } + + // Plugins view (default) const { plugins, pluginsTotal, marketplaceCollections, marketplaceCollectionPluginsMap, - isLoading, isFetchingNextPage, page, - } = useMarketplaceData() + } = marketplaceData + const pluginsCount = pluginsTotal || 0 const searchScopeOptions: Array<{ value: SearchScope, text: string, count: number }> = searchScopeOptionKeys.map(option => ({ value: option.value, diff --git a/web/app/components/plugins/marketplace/list/template-card.tsx b/web/app/components/plugins/marketplace/list/template-card.tsx new file mode 100644 index 0000000000..8c859bfe6c --- /dev/null +++ b/web/app/components/plugins/marketplace/list/template-card.tsx @@ -0,0 +1,92 @@ +'use client' + +import type { Template } from '../types' +import { useLocale } from '#i18n' +import Image from 'next/image' +import * as React from 'react' +import { getLanguage } from '@/i18n-config/language' +import { cn } from '@/utils/classnames' + +type TemplateCardProps = { + template: Template + className?: string +} + +const TemplateCardComponent = ({ + template, + className, +}: TemplateCardProps) => { + const locale = useLocale() + const { name, description, icon, tags, author } = template + + const descriptionText = description[getLanguage(locale)] || description.en_US || '' + + return ( +
+ {/* Header */} +
+
+ {icon + ? ( + {name} + ) + : ( + 📄 + )} +
+
+
+ {name} +
+
+ by + {' '} + {author} +
+
+
+ + {/* Description */} +
+ {descriptionText} +
+ + {/* Tags */} + {tags && tags.length > 0 && ( +
+ {tags.slice(0, 3).map(tag => ( + + {tag} + + ))} + {tags.length > 3 && ( + + + + {tags.length - 3} + + )} +
+ )} +
+ ) +} + +const TemplateCard = React.memo(TemplateCardComponent) + +export default TemplateCard diff --git a/web/app/components/plugins/marketplace/list/template-list.tsx b/web/app/components/plugins/marketplace/list/template-list.tsx new file mode 100644 index 0000000000..00f0ba4062 --- /dev/null +++ b/web/app/components/plugins/marketplace/list/template-list.tsx @@ -0,0 +1,129 @@ +'use client' + +import type { Template, TemplateCollection } from '../types' +import { useLocale, useTranslation } from '#i18n' +import { RiArrowRightSLine } from '@remixicon/react' +import { getLanguage } from '@/i18n-config/language' +import Empty from '../empty' +import Carousel from './carousel' +import TemplateCard from './template-card' + +type TemplateListProps = { + templateCollections: TemplateCollection[] + templateCollectionTemplatesMap: Record + cardContainerClassName?: string +} + +const FEATURED_COLLECTION_NAME = 'featured' +const GRID_DISPLAY_LIMIT = 8 + +const TemplateList = ({ + templateCollections, + templateCollectionTemplatesMap, + cardContainerClassName, +}: TemplateListProps) => { + const { t } = useTranslation() + const locale = useLocale() + + const renderTemplateCard = (template: Template) => { + return ( + + ) + } + + const renderFeaturedCarousel = (collection: TemplateCollection, templates: Template[]) => { + // Featured collection: 2-row carousel with auto-play + const rows: Template[][] = [] + for (let i = 0; i < templates.length; i += 2) { + rows.push(templates.slice(i, i + 2)) + } + + return ( + 8} + showPagination={templates.length > 8} + autoPlay={templates.length > 8} + autoPlayInterval={5000} + > + {rows.map(columnTemplates => ( +
+ {columnTemplates.map(template => ( +
+ {renderTemplateCard(template)} +
+ ))} +
+ ))} +
+ ) + } + + const renderGridCollection = (collection: TemplateCollection, templates: Template[]) => { + const displayTemplates = templates.slice(0, GRID_DISPLAY_LIMIT) + + return ( +
+ {displayTemplates.map(template => ( +
+ {renderTemplateCard(template)} +
+ ))} +
+ ) + } + + const collectionsWithTemplates = templateCollections.filter((collection) => { + return templateCollectionTemplatesMap[collection.name]?.length + }) + + if (collectionsWithTemplates.length === 0) { + return + } + + return ( + <> + { + collectionsWithTemplates.map((collection) => { + const templates = templateCollectionTemplatesMap[collection.name] + const isFeaturedCollection = collection.name === FEATURED_COLLECTION_NAME + const showViewMore = collection.searchable && (isFeaturedCollection || templates.length > GRID_DISPLAY_LIMIT) + + return ( +
+
+
+
{collection.label[getLanguage(locale)]}
+
{collection.description[getLanguage(locale)]}
+
+ {showViewMore && ( +
+ {t('marketplace.viewMore', { ns: 'plugin' })} + +
+ )} +
+ {isFeaturedCollection + ? renderFeaturedCarousel(collection, templates) + : renderGridCollection(collection, templates)} +
+ ) + }) + } + + ) +} + +export default TemplateList diff --git a/web/app/components/plugins/marketplace/query.ts b/web/app/components/plugins/marketplace/query.ts index 35d99a2bd5..3dcbf8c226 100644 --- a/web/app/components/plugins/marketplace/query.ts +++ b/web/app/components/plugins/marketplace/query.ts @@ -1,8 +1,8 @@ -import type { PluginsSearchParams } from './types' +import type { PluginsSearchParams, TemplateSearchParams } from './types' import type { MarketPlaceInputs } from '@/contract/router' import { useInfiniteQuery, useQuery } from '@tanstack/react-query' import { marketplaceQuery } from '@/service/client' -import { getMarketplaceCollectionsAndPlugins, getMarketplacePlugins } from './utils' +import { getMarketplaceCollectionsAndPlugins, getMarketplacePlugins, getMarketplaceTemplateCollectionsAndTemplates, getMarketplaceTemplates } from './utils' export function useMarketplaceCollectionsAndPlugins( collectionsParams: MarketPlaceInputs['collections']['query'], @@ -13,6 +13,15 @@ export function useMarketplaceCollectionsAndPlugins( }) } +export function useMarketplaceTemplateCollectionsAndTemplates( + query?: { page?: number, page_size?: number, condition?: string }, +) { + return useQuery({ + queryKey: marketplaceQuery.templateCollections.list.queryKey({ input: { query } }), + queryFn: ({ signal }) => getMarketplaceTemplateCollectionsAndTemplates(query, { signal }), + }) +} + export function useMarketplacePlugins( queryParams: PluginsSearchParams | undefined, ) { @@ -33,3 +42,23 @@ export function useMarketplacePlugins( enabled: queryParams !== undefined, }) } + +export function useMarketplaceTemplates( + queryParams: TemplateSearchParams | undefined, +) { + return useInfiniteQuery({ + queryKey: marketplaceQuery.templates.searchAdvanced.queryKey({ + input: { + body: queryParams!, + }, + }), + queryFn: ({ pageParam = 1, signal }) => getMarketplaceTemplates(queryParams, pageParam, signal), + getNextPageParam: (lastPage) => { + const nextPage = lastPage.page + 1 + const loaded = lastPage.page * lastPage.page_size + return loaded < (lastPage.total || 0) ? nextPage : undefined + }, + initialPageParam: 1, + enabled: queryParams !== undefined, + }) +} diff --git a/web/app/components/plugins/marketplace/state.ts b/web/app/components/plugins/marketplace/state.ts index 4954acd60c..303ae168aa 100644 --- a/web/app/components/plugins/marketplace/state.ts +++ b/web/app/components/plugins/marketplace/state.ts @@ -1,13 +1,18 @@ -import type { PluginsSearchParams } from './types' +import type { PluginsSearchParams, TemplateSearchParams } from './types' import { useDebounce } from 'ahooks' +import { useSearchParams } from 'next/navigation' import { useCallback, useMemo } from 'react' import { useActivePluginType, useFilterPluginTags, useMarketplaceSearchMode, useMarketplaceSortValue, useSearchPluginText } from './atoms' import { PLUGIN_TYPE_SEARCH_MAP } from './constants' import { useMarketplaceContainerScroll } from './hooks' -import { useMarketplaceCollectionsAndPlugins, useMarketplacePlugins } from './query' +import { useMarketplaceCollectionsAndPlugins, useMarketplacePlugins, useMarketplaceTemplateCollectionsAndTemplates, useMarketplaceTemplates } from './query' import { getCollectionsParams, getMarketplaceListFilterType } from './utils' -export function useMarketplaceData() { +/** + * Hook for plugins marketplace data + * Only fetches plugins-related data + */ +export function usePluginsMarketplaceData() { const [searchPluginTextOriginal] = useSearchPluginText() const searchPluginText = useDebounce(searchPluginTextOriginal, { wait: 500 }) const [filterPluginTags] = useFilterPluginTags() @@ -53,3 +58,133 @@ export function useMarketplaceData() { isFetchingNextPage, } } + +/** + * Hook for templates marketplace data + * Only fetches templates-related data + */ +export function useTemplatesMarketplaceData() { + // Reuse existing atoms for search and sort + const [searchTextOriginal] = useSearchPluginText() + const searchText = useDebounce(searchTextOriginal, { wait: 500 }) + const [activeCategory] = useActivePluginType() + + // Template collections query (for non-search mode) + const templateCollectionsQuery = useMarketplaceTemplateCollectionsAndTemplates() + + // Sort value + const sort = useMarketplaceSortValue() + + // Search mode: when there's search text or non-default category + const isSearchMode = !!searchText || (activeCategory !== PLUGIN_TYPE_SEARCH_MAP.all) + + // Build query params for search mode + const queryParams = useMemo((): TemplateSearchParams | undefined => { + if (!isSearchMode) + return undefined + return { + query: searchText, + categories: activeCategory === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : [activeCategory], + sort_by: sort.sortBy, + sort_order: sort.sortOrder, + } + }, [isSearchMode, searchText, activeCategory, sort]) + + // Templates search query (for search mode) + const templatesQuery = useMarketplaceTemplates(queryParams) + const { hasNextPage, fetchNextPage, isFetching, isFetchingNextPage } = templatesQuery + + // Pagination handler + const handlePageChange = useCallback(() => { + if (hasNextPage && !isFetching) + fetchNextPage() + }, [fetchNextPage, hasNextPage, isFetching]) + + // Scroll pagination + useMarketplaceContainerScroll(handlePageChange) + + // Compute flat templates list from collection map (for non-search mode) + const { collectionTemplates, collectionTemplatesTotal } = useMemo(() => { + const templateCollectionTemplatesMap = templateCollectionsQuery.data?.templateCollectionTemplatesMap + if (!templateCollectionTemplatesMap) { + return { collectionTemplates: undefined, collectionTemplatesTotal: 0 } + } + + const allTemplates = Object.values(templateCollectionTemplatesMap).flat() + // Deduplicate templates by template_id + const uniqueTemplates = allTemplates.filter( + (template, index, self) => index === self.findIndex(t => t.template_id === template.template_id), + ) + + return { + collectionTemplates: uniqueTemplates, + collectionTemplatesTotal: uniqueTemplates.length, + } + }, [templateCollectionsQuery.data?.templateCollectionTemplatesMap]) + + // Return search results when in search mode, otherwise return collection data + if (isSearchMode) { + return { + isSearchMode, + templateCollections: undefined, + templateCollectionTemplatesMap: undefined, + templates: templatesQuery.data?.pages.flatMap(page => page.templates), + templatesTotal: templatesQuery.data?.pages[0]?.total, + page: templatesQuery.data?.pages.length || 1, + isLoading: templatesQuery.isLoading, + isFetchingNextPage, + } + } + + return { + isSearchMode, + templateCollections: templateCollectionsQuery.data?.templateCollections, + templateCollectionTemplatesMap: templateCollectionsQuery.data?.templateCollectionTemplatesMap, + templates: collectionTemplates, + templatesTotal: collectionTemplatesTotal, + page: 1, + isLoading: templateCollectionsQuery.isLoading, + isFetchingNextPage: false, + } +} + +/** + * Main hook that routes to appropriate data based on creationType + * Returns either plugins or templates data based on URL parameter + */ +export function useMarketplaceData() { + const searchParams = useSearchParams() + const creationType = (searchParams.get('creationType') || 'plugins') as 'plugins' | 'templates' + + const pluginsData = usePluginsMarketplaceData() + const templatesData = useTemplatesMarketplaceData() + + if (creationType === 'templates') { + return { + creationType, + isSearchMode: templatesData.isSearchMode, + // Templates-specific fields + templateCollections: templatesData.templateCollections, + templateCollectionTemplatesMap: templatesData.templateCollectionTemplatesMap, + templates: templatesData.templates, + templatesTotal: templatesData.templatesTotal, + page: templatesData.page, + isLoading: templatesData.isLoading, + isFetchingNextPage: templatesData.isFetchingNextPage, + } + } + + // Default: plugins + return { + creationType, + isSearchMode: false, // plugins uses useMarketplaceSearchMode separately + // Plugins-specific fields + marketplaceCollections: pluginsData.marketplaceCollections, + marketplaceCollectionPluginsMap: pluginsData.marketplaceCollectionPluginsMap, + plugins: pluginsData.plugins, + pluginsTotal: pluginsData.pluginsTotal, + page: pluginsData.page, + isLoading: pluginsData.isLoading, + isFetchingNextPage: pluginsData.isFetchingNextPage, + } +} diff --git a/web/app/components/plugins/marketplace/types.ts b/web/app/components/plugins/marketplace/types.ts index e4e2dbd935..6c204d1b7f 100644 --- a/web/app/components/plugins/marketplace/types.ts +++ b/web/app/components/plugins/marketplace/types.ts @@ -56,4 +56,131 @@ export type SearchParams = { q?: string tags?: string category?: string + creationType?: string +} + +export type TemplateCollection = { + id: string + name: string + label: Record + description: Record + conditions: string[] + searchable: boolean + search_params?: SearchParamsFromCollection + created_at?: string + updated_at?: string +} + +export type Template = { + template_id: string + name: string + description: Record + icon: string + tags: string[] + author: string + created_at: string + updated_at: string +} + +export type CreateTemplateCollectionRequest = { + name: string + description: Record + label: Record + conditions: string[] + searchable: boolean + search_params: SearchParamsFromCollection +} + +export type GetCollectionTemplatesRequest = { + categories?: string[] + exclude?: string[] + limit?: number +} + +export type AddTemplateToCollectionRequest = { + template_id: string +} + +export type BatchAddTemplatesToCollectionRequest = { + template_id: string +}[] + +// Creator types +export type Creator = { + email: string + name: string + display_name: string + unique_handle: string + display_email: string + description: string + avatar: string + social_links: string[] + status: 'active' | 'inactive' + public?: boolean + plugin_count?: number + template_count?: number + created_at: string + updated_at: string +} + +export type CreatorSearchParams = { + query?: string + page?: number + page_size?: number + categories?: string[] + sort_by?: string + sort_order?: string +} + +export type CreatorSearchResponse = { + creators: Creator[] + total: number +} + +export type SyncCreatorProfileRequest = { + email: string + name?: string + display_name?: string + unique_handle: string + display_email?: string + description?: string + avatar?: string + social_links?: string[] + status?: 'active' | 'inactive' +} + +// Template Detail (full template info from API) +export type TemplateDetail = { + id: string + publisher_type: 'individual' | 'organization' + publisher_unique_handle: string + creator_email: string + template_name: string + icon: string + icon_background: string + icon_file_key: string + dsl_file_key: string + categories: string[] + overview: string + readme: string + partner_link: string + status: 'published' | 'draft' | 'pending' | 'rejected' + review_comment: string + created_at: string + updated_at: string +} + +export type TemplatesListResponse = { + templates: TemplateDetail[] + total: number +} + +export type TemplateSearchParams = { + query?: string + page?: number + page_size?: number + categories?: string[] + sort_by?: string + sort_order?: string + languages?: string[] } diff --git a/web/app/components/plugins/marketplace/utils.ts b/web/app/components/plugins/marketplace/utils.ts index 01f3c59284..34c287a86d 100644 --- a/web/app/components/plugins/marketplace/utils.ts +++ b/web/app/components/plugins/marketplace/utils.ts @@ -3,6 +3,10 @@ import type { CollectionsAndPluginsSearchParams, MarketplaceCollection, PluginsSearchParams, + Template, + TemplateCollection, + TemplateDetail, + TemplateSearchParams, } from '@/app/components/plugins/marketplace/types' import type { Plugin } from '@/app/components/plugins/types' import { PluginCategoryEnum } from '@/app/components/plugins/types' @@ -112,6 +116,49 @@ export const getMarketplaceCollectionsAndPlugins = async ( } } +export const getMarketplaceTemplateCollectionsAndTemplates = async ( + query?: { page?: number, page_size?: number, condition?: string }, + options?: MarketplaceFetchOptions, +) => { + let templateCollections: TemplateCollection[] = [] + let templateCollectionTemplatesMap: Record = {} + + try { + const res = await marketplaceClient.templateCollections.list({ + query: { + ...query, + page: 1, + page_size: 100, + }, + }, { + signal: options?.signal, + }) + templateCollections = res.data || [] + + await Promise.all(templateCollections.map(async (collection) => { + try { + const templatesRes = await marketplaceClient.templateCollections.getTemplates({ + params: { collectionName: collection.name }, + body: { limit: 20 }, + }, { signal: options?.signal }) + templateCollectionTemplatesMap[collection.name] = (templatesRes.data || []) as Template[] + } + catch { + templateCollectionTemplatesMap[collection.name] = [] + } + })) + } + catch { + templateCollections = [] + templateCollectionTemplatesMap = {} + } + + return { + templateCollections, + templateCollectionTemplatesMap, + } +} + export const getMarketplacePlugins = async ( queryParams: PluginsSearchParams | undefined, pageParam: number, @@ -203,3 +250,61 @@ export function getCollectionsParams(category: ActivePluginType): CollectionsAnd type: getMarketplaceListFilterType(category), } } + +export const getMarketplaceTemplates = async ( + queryParams: TemplateSearchParams | undefined, + pageParam: number, + signal?: AbortSignal, +): Promise<{ + templates: TemplateDetail[] + total: number + page: number + page_size: number +}> => { + if (!queryParams) { + return { + templates: [] as TemplateDetail[], + total: 0, + page: 1, + page_size: 40, + } + } + + const { + query, + sort_by, + sort_order, + categories, + languages, + page_size = 40, + } = queryParams + + try { + const res = await marketplaceClient.templates.searchAdvanced({ + body: { + page: pageParam, + page_size, + query, + sort_by, + sort_order, + categories, + languages, + }, + }, { signal }) + + return { + templates: res.data?.templates || [], + total: res.data?.total || 0, + page: pageParam, + page_size, + } + } + catch { + return { + templates: [], + total: 0, + page: pageParam, + page_size, + } + } +} diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index c3dce36371..b0cc76c295 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -30,8 +30,8 @@ import { } from './context' import DebugInfo from './debug-info' import InstallPluginDropdown from './install-plugin-dropdown' +import { SubmitRequestDropdown } from './nav-operations' import PluginTasks from './plugin-tasks' -import SubmitRequestDropdown from './submit-request-dropdown' import useReferenceSetting from './use-reference-setting' import { useUploader } from './use-uploader' diff --git a/web/app/components/plugins/plugin-page/submit-request-dropdown.tsx b/web/app/components/plugins/plugin-page/nav-operations.tsx similarity index 57% rename from web/app/components/plugins/plugin-page/submit-request-dropdown.tsx rename to web/app/components/plugins/plugin-page/nav-operations.tsx index e4318e6f3f..91c05fd65e 100644 --- a/web/app/components/plugins/plugin-page/submit-request-dropdown.tsx +++ b/web/app/components/plugins/plugin-page/nav-operations.tsx @@ -1,9 +1,11 @@ 'use client' -import { RiBookOpenLine, RiGithubLine } from '@remixicon/react' +import { RiBookOpenLine, RiGithubLine, RiLayoutGridLine, RiPuzzle2Line } from '@remixicon/react' import Link from 'next/link' +import { useSearchParams } from 'next/navigation' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import Button from '@/app/components/base/button' +import Badge from '@/app/components/base/badge' +import Button, { buttonVariants } from '@/app/components/base/button' import { PortalToFollowElem, PortalToFollowElemContent, @@ -31,7 +33,7 @@ const DropdownItem = ({ href, icon, text, onClick }: DropdownItemProps) => ( ) -const SubmitRequestDropdown = () => { +export const SubmitRequestDropdown = () => { const { t } = useTranslation() const [open, setOpen] = useState(false) const docLink = useDocLink() @@ -54,7 +56,6 @@ const SubmitRequestDropdown = () => { {t('requestSubmitPlugin', { ns: 'plugin' })} - {/* */} @@ -77,4 +78,42 @@ const SubmitRequestDropdown = () => { ) } -export default SubmitRequestDropdown +export const CreationTypeTabs = () => { + const { t } = useTranslation() + const searchParams = useSearchParams() + const creationType = searchParams.get('creationType') || 'plugins' + + return ( +
+ + + + {t('plugins', { ns: 'plugin' })} + + + + + + {t('templates', { ns: 'plugin' })} + + + NEW + + +
+ ) +} diff --git a/web/contract/marketplace.ts b/web/contract/marketplace.ts index 3573ba5c24..4736e2c3f5 100644 --- a/web/contract/marketplace.ts +++ b/web/contract/marketplace.ts @@ -1,4 +1,21 @@ -import type { CollectionsAndPluginsSearchParams, MarketplaceCollection, PluginsSearchParams } from '@/app/components/plugins/marketplace/types' +import type { + AddTemplateToCollectionRequest, + BatchAddTemplatesToCollectionRequest, + CollectionsAndPluginsSearchParams, + CreateTemplateCollectionRequest, + Creator, + CreatorSearchParams, + CreatorSearchResponse, + GetCollectionTemplatesRequest, + MarketplaceCollection, + PluginsSearchParams, + SyncCreatorProfileRequest, + Template, + TemplateCollection, + TemplateDetail, + TemplateSearchParams, + TemplatesListResponse, +} from '@/app/components/plugins/marketplace/types' import type { Plugin, PluginsFromMarketplaceResponse } from '@/app/components/plugins/types' import { type } from '@orpc/contract' import { base } from './base' @@ -54,3 +71,318 @@ export const searchAdvancedContract = base body: Omit }>()) .output(type<{ data: PluginsFromMarketplaceResponse }>()) + +export const templateCollectionsContract = base + .route({ + path: '/template-collections', + method: 'GET', + }) + .input( + type<{ + query?: { + page?: number + page_size?: number + condition?: string + } + }>(), + ) + .output( + type<{ + data?: TemplateCollection[] + has_more?: boolean + limit?: number + page?: number + total?: number + }>(), + ) + +export const createTemplateCollectionContract = base + .route({ + path: '/template-collections', + method: 'POST', + }) + .input( + type<{ + body: CreateTemplateCollectionRequest + }>(), + ) + .output(type()) + +export const getTemplateCollectionContract = base + .route({ + path: '/template-collections/{collectionName}', + method: 'GET', + }) + .input( + type<{ + params: { + collectionName: string + } + }>(), + ) + .output(type()) + +export const deleteTemplateCollectionContract = base + .route({ + path: '/template-collections/{collectionName}', + method: 'DELETE', + }) + .input( + type<{ + params: { + collectionName: string + } + }>(), + ) + .output(type()) + +export const getCollectionTemplatesContract = base + .route({ + path: '/template-collections/{collectionName}/templates', + method: 'POST', + }) + .input( + type<{ + params: { + collectionName: string + } + body?: GetCollectionTemplatesRequest + }>(), + ) + .output( + type<{ + data?: Template[] + }>(), + ) + +export const addTemplateToCollectionContract = base + .route({ + path: '/template-collections/{collectionName}/templates', + method: 'PUT', + }) + .input( + type<{ + params: { + collectionName: string + } + body: AddTemplateToCollectionRequest + }>(), + ) + .output(type()) + +export const batchAddTemplatesToCollectionContract = base + .route({ + path: '/template-collections/{collectionName}/templates/batch-add', + method: 'POST', + }) + .input( + type<{ + params: { + collectionName: string + } + body: BatchAddTemplatesToCollectionRequest + }>(), + ) + .output(type()) + +export const clearCollectionTemplatesContract = base + .route({ + path: '/template-collections/{collectionName}/templates/clear', + method: 'PUT', + }) + .input( + type<{ + params: { + collectionName: string + } + }>(), + ) + .output(type()) + +// Creators contracts +export const getCreatorByHandleContract = base + .route({ + path: '/creators/{uniqueHandle}', + method: 'GET', + }) + .input( + type<{ + params: { + uniqueHandle: string + } + }>(), + ) + .output( + type<{ + data?: { + creator?: Creator + } + }>(), + ) + +export const getCreatorAvatarContract = base + .route({ + path: '/creators/{uniqueHandle}/avatar', + method: 'GET', + }) + .input( + type<{ + params: { + uniqueHandle: string + } + }>(), + ) + .output(type()) + +export const syncCreatorProfileContract = base + .route({ + path: '/creators/sync/profile', + method: 'POST', + }) + .input( + type<{ + body: SyncCreatorProfileRequest + }>(), + ) + .output( + type<{ + data?: { + creator?: Creator + } + }>(), + ) + +export const syncCreatorAvatarContract = base + .route({ + path: '/creators/sync/avatar', + method: 'POST', + }) + .input( + type<{ + body: FormData + }>(), + ) + .output(type()) + +export const searchCreatorsAdvancedContract = base + .route({ + path: '/creators/search/advanced', + method: 'POST', + }) + .input( + type<{ + body: CreatorSearchParams + }>(), + ) + .output( + type<{ + data?: CreatorSearchResponse + }>(), + ) + +// Templates public endpoints +export const getTemplatesListContract = base + .route({ + path: '/templates', + method: 'GET', + }) + .input( + type<{ + query?: { + page?: number + page_size?: number + categories?: string + } + }>(), + ) + .output( + type<{ + data?: TemplatesListResponse + }>(), + ) + +export const getTemplateByIdContract = base + .route({ + path: '/templates/{templateId}', + method: 'GET', + }) + .input( + type<{ + params: { + templateId: string + } + }>(), + ) + .output( + type<{ + data?: TemplateDetail + }>(), + ) + +export const getTemplateDslFileContract = base + .route({ + path: '/templates/{templateId}/file', + method: 'GET', + }) + .input( + type<{ + params: { + templateId: string + } + }>(), + ) + .output(type()) + +export const searchTemplatesBasicContract = base + .route({ + path: '/templates/search/basic', + method: 'POST', + }) + .input( + type<{ + body: TemplateSearchParams + }>(), + ) + .output( + type<{ + data?: TemplatesListResponse + }>(), + ) + +export const searchTemplatesAdvancedContract = base + .route({ + path: '/templates/search/advanced', + method: 'POST', + }) + .input( + type<{ + body: TemplateSearchParams + }>(), + ) + .output( + type<{ + data?: TemplatesListResponse + }>(), + ) + +export const getPublisherTemplatesContract = base + .route({ + path: '/templates/publisher/{uniqueHandle}', + method: 'GET', + }) + .input( + type<{ + params: { + uniqueHandle: string + } + query?: { + page?: number + page_size?: number + } + }>(), + ) + .output( + type<{ + data?: TemplatesListResponse + }>(), + ) diff --git a/web/contract/router.ts b/web/contract/router.ts index 33499b106f..2c1391b9ec 100644 --- a/web/contract/router.ts +++ b/web/contract/router.ts @@ -19,12 +19,60 @@ import { triggerSubscriptionVerifyContract, } from './console/trigger' import { trialAppDatasetsContract, trialAppInfoContract, trialAppParametersContract, trialAppWorkflowsContract } from './console/try-app' -import { collectionPluginsContract, collectionsContract, searchAdvancedContract } from './marketplace' +import { + addTemplateToCollectionContract, + batchAddTemplatesToCollectionContract, + clearCollectionTemplatesContract, + collectionPluginsContract, + collectionsContract, + createTemplateCollectionContract, + deleteTemplateCollectionContract, + getCollectionTemplatesContract, + getCreatorAvatarContract, + getCreatorByHandleContract, + getPublisherTemplatesContract, + getTemplateByIdContract, + getTemplateCollectionContract, + getTemplateDslFileContract, + getTemplatesListContract, + searchAdvancedContract, + searchCreatorsAdvancedContract, + searchTemplatesAdvancedContract, + searchTemplatesBasicContract, + syncCreatorAvatarContract, + syncCreatorProfileContract, + templateCollectionsContract, +} from './marketplace' export const marketplaceRouterContract = { collections: collectionsContract, collectionPlugins: collectionPluginsContract, searchAdvanced: searchAdvancedContract, + templateCollections: { + list: templateCollectionsContract, + create: createTemplateCollectionContract, + get: getTemplateCollectionContract, + delete: deleteTemplateCollectionContract, + getTemplates: getCollectionTemplatesContract, + addTemplate: addTemplateToCollectionContract, + batchAddTemplates: batchAddTemplatesToCollectionContract, + clearTemplates: clearCollectionTemplatesContract, + }, + creators: { + getByHandle: getCreatorByHandleContract, + getAvatar: getCreatorAvatarContract, + syncProfile: syncCreatorProfileContract, + syncAvatar: syncCreatorAvatarContract, + searchAdvanced: searchCreatorsAdvancedContract, + }, + templates: { + list: getTemplatesListContract, + getById: getTemplateByIdContract, + getDslFile: getTemplateDslFileContract, + searchBasic: searchTemplatesBasicContract, + searchAdvanced: searchTemplatesAdvancedContract, + getPublisherTemplates: getPublisherTemplatesContract, + }, } export type MarketPlaceInputs = InferContractRouterInputs diff --git a/web/i18n/en-US/plugin.json b/web/i18n/en-US/plugin.json index 8ed9732586..9cbef2d774 100644 --- a/web/i18n/en-US/plugin.json +++ b/web/i18n/en-US/plugin.json @@ -228,6 +228,8 @@ "pluginInfoModal.release": "Release", "pluginInfoModal.repository": "Repository", "pluginInfoModal.title": "Plugin info", + "plugins": "Plugins", + "templates": "Templates", "privilege.admins": "Admins", "privilege.everyone": "Everyone", "privilege.noone": "No one", diff --git a/web/i18n/zh-Hans/plugin.json b/web/i18n/zh-Hans/plugin.json index d0f9a113d4..4722d22c32 100644 --- a/web/i18n/zh-Hans/plugin.json +++ b/web/i18n/zh-Hans/plugin.json @@ -228,6 +228,8 @@ "pluginInfoModal.release": "发布版本", "pluginInfoModal.repository": "仓库", "pluginInfoModal.title": "插件信息", + "plugins": "插件", + "templates": "模板", "privilege.admins": "管理员", "privilege.everyone": "所有人", "privilege.noone": "无人",