mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 05:56:31 +08:00
feat: add templates marketplace functionality
This commit is contained in:
parent
41c1d981a1
commit
a59023f75b
@ -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<SearchScope>('all')
|
||||
|
||||
const marketplaceData = useMarketplaceData()
|
||||
const {
|
||||
creationType,
|
||||
isLoading,
|
||||
} = marketplaceData
|
||||
|
||||
// Templates view
|
||||
if (creationType === 'templates') {
|
||||
const { templateCollections, templateCollectionTemplatesMap } = marketplaceData
|
||||
return (
|
||||
<div
|
||||
style={{ scrollbarGutter: 'stable' }}
|
||||
className="relative flex grow flex-col bg-background-default-subtle px-12 py-2"
|
||||
>
|
||||
{
|
||||
isLoading && (
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isLoading && (
|
||||
<TemplateList
|
||||
templateCollections={templateCollections || []}
|
||||
templateCollectionTemplatesMap={templateCollectionTemplatesMap || {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
@ -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 (
|
||||
<div className={cn(
|
||||
'hover-bg-components-panel-on-panel-item-bg relative cursor-pointer overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-background-default-lighter">
|
||||
{icon
|
||||
? (
|
||||
<Image
|
||||
src={icon}
|
||||
alt={name}
|
||||
width={24}
|
||||
height={24}
|
||||
className="h-6 w-6 object-contain"
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<span className="text-lg">📄</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="flex h-5 items-center">
|
||||
<span className="system-md-semibold truncate text-text-secondary">{name}</span>
|
||||
</div>
|
||||
<div className="system-2xs-medium-uppercase mt-0.5 truncate text-text-tertiary">
|
||||
by
|
||||
{' '}
|
||||
{author}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div
|
||||
className="system-xs-regular mt-3 line-clamp-2 text-text-tertiary"
|
||||
title={descriptionText}
|
||||
>
|
||||
{descriptionText}
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{tags && tags.length > 0 && (
|
||||
<div className="mt-3 flex flex-wrap gap-1">
|
||||
{tags.slice(0, 3).map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="system-2xs-medium-uppercase bg-components-badge-bg-gray rounded-md px-1.5 py-0.5 text-text-tertiary"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{tags.length > 3 && (
|
||||
<span className="system-2xs-medium-uppercase text-text-quaternary">
|
||||
+
|
||||
{tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TemplateCard = React.memo(TemplateCardComponent)
|
||||
|
||||
export default TemplateCard
|
||||
129
web/app/components/plugins/marketplace/list/template-list.tsx
Normal file
129
web/app/components/plugins/marketplace/list/template-list.tsx
Normal file
@ -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<string, Template[]>
|
||||
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 (
|
||||
<TemplateCard
|
||||
key={template.template_id}
|
||||
template={template}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Carousel
|
||||
className={cardContainerClassName}
|
||||
showNavigation={templates.length > 8}
|
||||
showPagination={templates.length > 8}
|
||||
autoPlay={templates.length > 8}
|
||||
autoPlayInterval={5000}
|
||||
>
|
||||
{rows.map(columnTemplates => (
|
||||
<div
|
||||
key={`column-${columnTemplates[0]?.template_id}`}
|
||||
className="flex w-[calc((100%-0px)/1)] shrink-0 flex-col gap-3 sm:w-[calc((100%-12px)/2)] lg:w-[calc((100%-24px)/3)] xl:w-[calc((100%-36px)/4)]"
|
||||
style={{ scrollSnapAlign: 'start' }}
|
||||
>
|
||||
{columnTemplates.map(template => (
|
||||
<div key={template.template_id}>
|
||||
{renderTemplateCard(template)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
)
|
||||
}
|
||||
|
||||
const renderGridCollection = (collection: TemplateCollection, templates: Template[]) => {
|
||||
const displayTemplates = templates.slice(0, GRID_DISPLAY_LIMIT)
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{displayTemplates.map(template => (
|
||||
<div key={template.template_id}>
|
||||
{renderTemplateCard(template)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const collectionsWithTemplates = templateCollections.filter((collection) => {
|
||||
return templateCollectionTemplatesMap[collection.name]?.length
|
||||
})
|
||||
|
||||
if (collectionsWithTemplates.length === 0) {
|
||||
return <Empty />
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
key={collection.name}
|
||||
className="py-3"
|
||||
>
|
||||
<div className="mb-2 flex items-end justify-between">
|
||||
<div>
|
||||
<div className="title-xl-semi-bold text-text-primary">{collection.label[getLanguage(locale)]}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{collection.description[getLanguage(locale)]}</div>
|
||||
</div>
|
||||
{showViewMore && (
|
||||
<div
|
||||
className="system-xs-medium flex cursor-pointer items-center text-text-accent"
|
||||
>
|
||||
{t('marketplace.viewMore', { ns: 'plugin' })}
|
||||
<RiArrowRightSLine className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isFeaturedCollection
|
||||
? renderFeaturedCarousel(collection, templates)
|
||||
: renderGridCollection(collection, templates)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TemplateList
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,4 +56,131 @@ export type SearchParams = {
|
||||
q?: string
|
||||
tags?: string
|
||||
category?: string
|
||||
creationType?: string
|
||||
}
|
||||
|
||||
export type TemplateCollection = {
|
||||
id: string
|
||||
name: string
|
||||
label: Record<string, string>
|
||||
description: Record<string, string>
|
||||
conditions: string[]
|
||||
searchable: boolean
|
||||
search_params?: SearchParamsFromCollection
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export type Template = {
|
||||
template_id: string
|
||||
name: string
|
||||
description: Record<string, string>
|
||||
icon: string
|
||||
tags: string[]
|
||||
author: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type CreateTemplateCollectionRequest = {
|
||||
name: string
|
||||
description: Record<string, string>
|
||||
label: Record<string, string>
|
||||
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[]
|
||||
}
|
||||
|
||||
@ -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<string, Template[]> = {}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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) => (
|
||||
</Link>
|
||||
)
|
||||
|
||||
const SubmitRequestDropdown = () => {
|
||||
export const SubmitRequestDropdown = () => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const docLink = useDocLink()
|
||||
@ -54,7 +56,6 @@ const SubmitRequestDropdown = () => {
|
||||
<span className="system-sm-medium">
|
||||
{t('requestSubmitPlugin', { ns: 'plugin' })}
|
||||
</span>
|
||||
{/* <RiArrowDownSLine className={cn("h-4 w-4 transition-transform", open && "rotate-180")} /> */}
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[1000]">
|
||||
@ -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 (
|
||||
<div className="flex items-center gap-1">
|
||||
<Link
|
||||
href="/?creationType=plugins"
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'flex items-center gap-1 px-3 py-2 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
creationType === 'plugins' && 'bg-state-base-hover text-text-secondary',
|
||||
)}
|
||||
>
|
||||
<RiPuzzle2Line className="h-4 w-4 shrink-0" />
|
||||
<span className="system-sm-medium">
|
||||
{t('plugins', { ns: 'plugin' })}
|
||||
</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/?creationType=templates"
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'flex items-center gap-1 px-3 py-2 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
creationType === 'templates' && 'bg-state-base-hover text-text-secondary',
|
||||
)}
|
||||
>
|
||||
<RiLayoutGridLine className="h-4 w-4 shrink-0" />
|
||||
<span className="system-sm-medium">
|
||||
{t('templates', { ns: 'plugin' })}
|
||||
</span>
|
||||
<Badge className="ml-1 h-4 rounded-[4px] border-none bg-saas-dify-blue-accessible px-1 text-[10px] font-bold leading-[14px] text-text-primary-on-surface">
|
||||
NEW
|
||||
</Badge>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -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<PluginsSearchParams, 'type'>
|
||||
}>())
|
||||
.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<TemplateCollection>())
|
||||
|
||||
export const getTemplateCollectionContract = base
|
||||
.route({
|
||||
path: '/template-collections/{collectionName}',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(
|
||||
type<{
|
||||
params: {
|
||||
collectionName: string
|
||||
}
|
||||
}>(),
|
||||
)
|
||||
.output(type<TemplateCollection>())
|
||||
|
||||
export const deleteTemplateCollectionContract = base
|
||||
.route({
|
||||
path: '/template-collections/{collectionName}',
|
||||
method: 'DELETE',
|
||||
})
|
||||
.input(
|
||||
type<{
|
||||
params: {
|
||||
collectionName: string
|
||||
}
|
||||
}>(),
|
||||
)
|
||||
.output(type<void>())
|
||||
|
||||
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<void>())
|
||||
|
||||
export const batchAddTemplatesToCollectionContract = base
|
||||
.route({
|
||||
path: '/template-collections/{collectionName}/templates/batch-add',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(
|
||||
type<{
|
||||
params: {
|
||||
collectionName: string
|
||||
}
|
||||
body: BatchAddTemplatesToCollectionRequest
|
||||
}>(),
|
||||
)
|
||||
.output(type<void>())
|
||||
|
||||
export const clearCollectionTemplatesContract = base
|
||||
.route({
|
||||
path: '/template-collections/{collectionName}/templates/clear',
|
||||
method: 'PUT',
|
||||
})
|
||||
.input(
|
||||
type<{
|
||||
params: {
|
||||
collectionName: string
|
||||
}
|
||||
}>(),
|
||||
)
|
||||
.output(type<void>())
|
||||
|
||||
// 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<Blob>())
|
||||
|
||||
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<void>())
|
||||
|
||||
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<Blob>())
|
||||
|
||||
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
|
||||
}>(),
|
||||
)
|
||||
|
||||
@ -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<typeof marketplaceRouterContract>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -228,6 +228,8 @@
|
||||
"pluginInfoModal.release": "发布版本",
|
||||
"pluginInfoModal.repository": "仓库",
|
||||
"pluginInfoModal.title": "插件信息",
|
||||
"plugins": "插件",
|
||||
"templates": "模板",
|
||||
"privilege.admins": "管理员",
|
||||
"privilege.everyone": "所有人",
|
||||
"privilege.noone": "无人",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user