diff --git a/web/app/components/plugins/marketplace/creator-profile/__tests__/creator-content.spec.tsx b/web/app/components/plugins/marketplace/creator-profile/__tests__/creator-content.spec.tsx index 39306668d88..b34d152ec5b 100644 --- a/web/app/components/plugins/marketplace/creator-profile/__tests__/creator-content.spec.tsx +++ b/web/app/components/plugins/marketplace/creator-profile/__tests__/creator-content.spec.tsx @@ -5,6 +5,20 @@ import { describe, expect, it, vi } from 'vitest' import { renderWithNuqs } from '@/test/nuqs-testing' import CreatorContent from '../creator-content' +const publisherMocks = vi.hoisted(() => ({ + fetchPublisherPluginPage: vi.fn(), + fetchPublisherTemplatePage: vi.fn(), +})) + +vi.mock('../publisher', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + fetchPublisherPluginPage: publisherMocks.fetchPublisherPluginPage, + fetchPublisherTemplatePage: publisherMocks.fetchPublisherTemplatePage, + } +}) + vi.mock('#i18n', async () => { const { withSelectorKey } = await import('@/test/i18n-mock') const translations: Record = { @@ -17,6 +31,8 @@ vi.mock('#i18n', async () => { 'marketplace.creatorProfile.sort.desc': 'Sort descending', 'marketplace.creatorProfile.type.plugin': 'Plugin', 'marketplace.creatorProfile.type.template': 'Template', + 'marketplace.creatorProfile.loadMore': 'Load more', + 'marketplace.creatorProfile.loadMoreFailed': "Couldn't load more creations.", } return { @@ -59,6 +75,10 @@ const creations = [ const cardNames = () => screen.getAllByRole('link').map((link) => link.getAttribute('aria-label')) describe('CreatorContent', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + it('writes sort into the URL and reorders the current cards', async () => { const user = userEvent.setup() const { onUrlUpdate } = renderWithNuqs( @@ -91,4 +111,53 @@ describe('CreatorContent', () => { expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('sort_by')).toBe('popularity') }) }) + + it('loads the next publisher pages when more creations exist', async () => { + const user = userEvent.setup() + publisherMocks.fetchPublisherPluginPage.mockResolvedValue({ + items: [ + { + type: 'plugin', + org: 'dify', + name: 'delta', + labels: { 'en-US': 'Delta' }, + brief: { 'en-US': 'Delta plugin' }, + install_count: 4, + created_at: '2026-01-04T00:00:00Z', + updated_at: '2026-02-04T00:00:00Z', + }, + ], + hasMore: false, + }) + publisherMocks.fetchPublisherTemplatePage.mockResolvedValue({ items: [], hasMore: false }) + + renderWithNuqs( + ({ type: 'link', href: `/creation/${creation.id}` })} + />, + ) + + await user.click(screen.getByRole('button', { name: 'Load more' })) + + await waitFor(() => { + expect(cardNames()).toContain('Delta') + }) + expect(publisherMocks.fetchPublisherPluginPage).toHaveBeenCalledWith({ + uniqueHandle: 'scarlettmao', + page: 2, + sortField: 'updatedAt', + sortOrder: 'desc', + }) + expect(publisherMocks.fetchPublisherTemplatePage).not.toHaveBeenCalled() + expect(screen.queryByRole('button', { name: 'Load more' })).not.toBeInTheDocument() + }) }) diff --git a/web/app/components/plugins/marketplace/creator-profile/__tests__/data.server.spec.ts b/web/app/components/plugins/marketplace/creator-profile/__tests__/data.server.spec.ts index c35b8ccc774..8aac42bfb92 100644 --- a/web/app/components/plugins/marketplace/creator-profile/__tests__/data.server.spec.ts +++ b/web/app/components/plugins/marketplace/creator-profile/__tests__/data.server.spec.ts @@ -164,35 +164,50 @@ describe('loadCreatorProfile', () => { expect(loaded?.viewModel.creations.map(({ kind }) => kind)).toEqual(['template', 'plugin']) }) - it('fetches remaining publisher pages until the reported total is loaded', async () => { - const extraPlugin = { - ...plugin, - name: 'extra', - plugin_id: 'dify/extra', - } as MarketplacePlugin - mocks.publisherPlugins - .mockResolvedValueOnce({ - data: { plugins: [plugin], total: 2 }, - }) - .mockResolvedValueOnce({ - data: { plugins: [extraPlugin], total: 2 }, - }) + it('loads only the first publisher page and reports remaining inventory', async () => { + mocks.publisherPlugins.mockResolvedValue({ + data: { + plugins: Array.from({ length: 40 }, (_, index) => ({ ...plugin, name: `p-${index}` })), + total: 90, + }, + }) + mocks.publisherTemplates.mockResolvedValue({ + data: { templates: [template], total: 1 }, + }) const loaded = await loadCreatorProfile({ uniqueHandle: 'paged-creator', locale: 'en-US', }) - expect(mocks.publisherPlugins).toHaveBeenNthCalledWith(1, { + expect(mocks.publisherPlugins).toHaveBeenCalledOnce() + expect(mocks.publisherPlugins).toHaveBeenCalledWith({ params: { uniqueHandle: 'paged-creator' }, query: { page: 1, page_size: 40, sort_by: 'version_updated_at', sort_order: 'DESC' }, }) - expect(mocks.publisherPlugins).toHaveBeenNthCalledWith(2, { - params: { uniqueHandle: 'paged-creator' }, - query: { page: 2, page_size: 40, sort_by: 'version_updated_at', sort_order: 'DESC' }, + expect(loaded?.inventory).toMatchObject({ + uniqueHandle: 'paged-creator', + pluginHasMore: true, + templateHasMore: false, + pluginNextPage: 2, }) - expect(loaded?.pluginsByCreationId['plugin:dify/search']).toBeDefined() - expect(loaded?.pluginsByCreationId['plugin:dify/extra']).toBeDefined() + expect(loaded?.viewModel.creations).toHaveLength(41) + }) + + it('does not treat a full first page as the complete inventory when total is missing', async () => { + mocks.publisherPlugins.mockResolvedValue({ + data: { + plugins: Array.from({ length: 40 }, (_, index) => ({ ...plugin, name: `p-${index}` })), + }, + }) + + const loaded = await loadCreatorProfile({ + uniqueHandle: 'uncounted-creator', + locale: 'en-US', + }) + + expect(mocks.publisherPlugins).toHaveBeenCalledOnce() + expect(loaded?.inventory.pluginHasMore).toBe(true) }) it('keeps successful creations when one publisher request fails', async () => { diff --git a/web/app/components/plugins/marketplace/creator-profile/__tests__/dify-profile.spec.tsx b/web/app/components/plugins/marketplace/creator-profile/__tests__/dify-profile.spec.tsx index 9760ad9591a..1eb2c318b94 100644 --- a/web/app/components/plugins/marketplace/creator-profile/__tests__/dify-profile.spec.tsx +++ b/web/app/components/plugins/marketplace/creator-profile/__tests__/dify-profile.spec.tsx @@ -99,6 +99,13 @@ const loadedProfile: LoadedCreatorProfile = { templatesByCreationId: { 'template:template-one': template, }, + inventory: { + uniqueHandle: 'creator', + pluginHasMore: false, + templateHasMore: false, + pluginNextPage: 2, + templateNextPage: 2, + }, } vi.mock('#i18n', async () => { diff --git a/web/app/components/plugins/marketplace/creator-profile/__tests__/model.spec.ts b/web/app/components/plugins/marketplace/creator-profile/__tests__/model.spec.ts index ff8a507998c..99696799f8c 100644 --- a/web/app/components/plugins/marketplace/creator-profile/__tests__/model.spec.ts +++ b/web/app/components/plugins/marketplace/creator-profile/__tests__/model.spec.ts @@ -192,4 +192,33 @@ describe('creator profile model', () => { expect(normalizeCreatorSocialLink('data:text/html,bad')).toBeNull() expect(normalizeCreatorSocialLink('mailto:test@example.com')).toBeNull() }) + + it('ignores non-string social links and template dependencies instead of throwing', () => { + expect(normalizeCreatorSocialLink({ href: 'https://x.com/x' })).toBeNull() + expect(normalizeCreatorSocialLink(null)).toBeNull() + + const viewModel = adaptCreatorProfile({ + creator: { + ...creator, + social_links: [{ href: 'https://x.com/x' }, 'github.com/evanz'] as unknown as string[], + }, + kind: 'individual', + locale: 'en-US', + avatarUrl: '/avatar', + backgroundUrl: '/background', + plugins: [], + templates: [ + { + ...template, + deps_plugins: [null, 'dify/search', ''] as unknown as string[], + }, + ], + resolvePluginIcon: () => '/plugin-icon', + resolveTemplateIcon: () => '', + resolveDependencyIcon: (id) => `/dependency/${id}`, + }) + + expect(viewModel.profile.socialLinks).toEqual([expect.objectContaining({ platform: 'github' })]) + expect(viewModel.creations[0]?.dependencyIcons).toEqual(['/dependency/dify/search']) + }) }) diff --git a/web/app/components/plugins/marketplace/creator-profile/creator-content.tsx b/web/app/components/plugins/marketplace/creator-profile/creator-content.tsx index 8665049314f..bc3a890fad2 100644 --- a/web/app/components/plugins/marketplace/creator-profile/creator-content.tsx +++ b/web/app/components/plugins/marketplace/creator-profile/creator-content.tsx @@ -1,11 +1,14 @@ 'use client' +import type { MarketplacePlugin, MarketplaceTemplate } from '@dify/contracts/marketplace' import type { CreatorCreation, CreatorCreationAction, + CreatorInventory, CreatorSortField, CreatorSortOrder, } from './model' +import type { Plugin } from '@/app/components/plugins/types' import { DropdownMenu, DropdownMenuContent, @@ -15,7 +18,7 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { parseAsStringEnum, useQueryStates } from 'nuqs' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from '#i18n' import CreationCard from './creation-card' import { @@ -24,10 +27,17 @@ import { DEFAULT_CREATOR_SORT_ORDER, sortCreatorCreations, } from './model' +import { fetchPublisherPluginPage, fetchPublisherTemplatePage, toCreatorRecords } from './publisher' type CreatorContentProps = { creations: CreatorCreation[] getCreationAction: (creation: CreatorCreation) => CreatorCreationAction + inventory?: CreatorInventory + locale?: string + onRecordsLoaded?: (records: { + pluginsByCreationId: Record + templatesByCreationId: Record + }) => void } const sortSearchOptions = { history: 'replace' as const, shallow: false, scroll: false } @@ -40,11 +50,34 @@ const creatorSortSearchParsers = { ), } -export default function CreatorContent({ creations, getCreationAction }: CreatorContentProps) { +export default function CreatorContent({ + creations, + getCreationAction, + inventory, + locale = 'en-US', + onRecordsLoaded, +}: CreatorContentProps) { const { t } = useTranslation() const [sort, setSort] = useQueryStates(creatorSortSearchParsers, sortSearchOptions) const sortField = sort.sort_by const sortOrder = sort.sort_order + const [sourceCreations, setSourceCreations] = useState(creations) + const [loadedCreations, setLoadedCreations] = useState(creations) + const [pluginHasMore, setPluginHasMore] = useState(inventory?.pluginHasMore ?? false) + const [templateHasMore, setTemplateHasMore] = useState(inventory?.templateHasMore ?? false) + const [pluginNextPage, setPluginNextPage] = useState(inventory?.pluginNextPage ?? 2) + const [templateNextPage, setTemplateNextPage] = useState(inventory?.templateNextPage ?? 2) + const [isLoadingMore, setIsLoadingMore] = useState(false) + const [loadMoreFailed, setLoadMoreFailed] = useState(false) + if (creations !== sourceCreations) { + setSourceCreations(creations) + setLoadedCreations(creations) + setPluginHasMore(inventory?.pluginHasMore ?? false) + setTemplateHasMore(inventory?.templateHasMore ?? false) + setPluginNextPage(inventory?.pluginNextPage ?? 2) + setTemplateNextPage(inventory?.templateNextPage ?? 2) + setLoadMoreFailed(false) + } const sortOptions: Array<{ value: CreatorSortField; label: string }> = [ { value: 'updatedAt', @@ -61,10 +94,61 @@ export default function CreatorContent({ creations, getCreationAction }: Creator ] const selectedSort = sortOptions.find((option) => option.value === sortField) ?? sortOptions[0]! const sortedCreations = useMemo( - () => sortCreatorCreations(creations, sortField, sortOrder), - [creations, sortField, sortOrder], + () => sortCreatorCreations(loadedCreations, sortField, sortOrder), + [loadedCreations, sortField, sortOrder], ) const nextSortOrder = sortOrder === 'desc' ? 'asc' : 'desc' + const hasMore = pluginHasMore || templateHasMore + const uniqueHandle = inventory?.uniqueHandle + + const loadMore = async () => { + if (!uniqueHandle || isLoadingMore || !hasMore) return + + setIsLoadingMore(true) + setLoadMoreFailed(false) + try { + const [pluginPage, templatePage] = await Promise.all([ + pluginHasMore + ? fetchPublisherPluginPage({ + uniqueHandle, + page: pluginNextPage, + sortField, + sortOrder, + }) + : Promise.resolve({ items: [] as MarketplacePlugin[], hasMore: false }), + templateHasMore + ? fetchPublisherTemplatePage({ + uniqueHandle, + page: templateNextPage, + sortField, + sortOrder, + }) + : Promise.resolve({ items: [] as MarketplaceTemplate[], hasMore: false }), + ]) + const records = toCreatorRecords({ + locale, + plugins: pluginPage.items, + templates: templatePage.items, + }) + setLoadedCreations((current) => { + const seen = new Set(current.map((creation) => creation.id)) + return [...current, ...records.creations.filter((creation) => !seen.has(creation.id))] + }) + if (pluginHasMore) { + setPluginHasMore(pluginPage.hasMore) + setPluginNextPage((page) => page + 1) + } + if (templateHasMore) { + setTemplateHasMore(templatePage.hasMore) + setTemplateNextPage((page) => page + 1) + } + onRecordsLoaded?.(records) + } catch { + setLoadMoreFailed(true) + } finally { + setIsLoadingMore(false) + } + } return (
$['marketplace.creatorProfile.empty'], { ns: 'plugin' })} )} + + {hasMore && ( +
+ + {loadMoreFailed && ( +

+ {t(($) => $['marketplace.creatorProfile.loadMoreFailed'], { ns: 'plugin' })} +

+ )} +
+ )}
) } diff --git a/web/app/components/plugins/marketplace/creator-profile/data.server.ts b/web/app/components/plugins/marketplace/creator-profile/data.server.ts index 6de6c8a4df2..38910290da6 100644 --- a/web/app/components/plugins/marketplace/creator-profile/data.server.ts +++ b/web/app/components/plugins/marketplace/creator-profile/data.server.ts @@ -1,42 +1,24 @@ -import type { - MarketplaceCreator, - MarketplaceOrganization, - MarketplacePlugin, - MarketplaceTemplate, -} from '@dify/contracts/marketplace' +import type { MarketplaceCreator, MarketplaceOrganization } from '@dify/contracts/marketplace' import type { CreatorSortField, CreatorSortOrder, LoadedCreatorProfile } from './model' import { cache } from 'react' import { MARKETPLACE_API_PREFIX } from '@/config' import { marketplaceClient } from '@/service/client' -import { getFormattedPlugin, getPluginIconInMarketplace } from '../utils' +import { getPluginIconInMarketplace } from '../utils' import { adaptCreatorProfile, parseCreatorSortField, parseCreatorSortOrder, sortCreatorCreations, - toPublisherSortQuery, } from './model' +import { + fetchPublisherPluginPage, + fetchPublisherTemplatePage, + getDependencyIcon, + getTemplateIcon, + toCreatorRecords, +} from './publisher' import 'server-only' -const PAGE_SIZE = 40 -const MAX_PAGES = 5 - -const fetchAllPublisherPages = async ( - fetchPage: (page: number) => Promise<{ items: T[]; total?: number }>, -) => { - const first = await fetchPage(1) - const items = [...first.items] - const total = first.total ?? items.length - - for (let page = 2; page <= MAX_PAGES && items.length < total; page++) { - const next = await fetchPage(page) - if (next.items.length === 0) break - items.push(...next.items) - } - - return items -} - const mapOrganizationToCreator = ( organization: MarketplaceOrganization, uniqueHandle: string, @@ -73,50 +55,6 @@ const getPublisher = async (uniqueHandle: string, publisherType?: string) => { return response.data?.creator } -const getPublisherPlugins = async ( - uniqueHandle: string, - sortField: CreatorSortField, - sortOrder: CreatorSortOrder, -) => { - const { plugins } = toPublisherSortQuery(sortField, sortOrder) - return fetchAllPublisherPages(async (page) => { - const response = await marketplaceClient.publisherPlugins({ - params: { uniqueHandle }, - query: { page, page_size: PAGE_SIZE, ...plugins }, - }) - return { - items: response.data?.plugins ?? [], - total: response.data?.total, - } - }) -} - -const getPublisherTemplates = async ( - uniqueHandle: string, - sortField: CreatorSortField, - sortOrder: CreatorSortOrder, -) => { - const { templates } = toPublisherSortQuery(sortField, sortOrder) - return fetchAllPublisherPages(async (page) => { - const response = await marketplaceClient.publisherTemplates({ - params: { uniqueHandle }, - query: { page, page_size: PAGE_SIZE, ...templates }, - }) - return { - items: response.data?.templates ?? [], - total: response.data?.total, - } - }) -} - -const getTemplateIcon = (template: MarketplaceTemplate) => - template.icon_file_key - ? `${MARKETPLACE_API_PREFIX}/templates/${encodeURIComponent(template.id)}/icon` - : '' - -const getDependencyIcon = (pluginId: string) => - `${MARKETPLACE_API_PREFIX}/plugins/${pluginId.split('/').map(encodeURIComponent).join('/')}/icon` - const loadCreatorProfileCached = cache( async ( uniqueHandle: string, @@ -127,18 +65,18 @@ const loadCreatorProfileCached = cache( ): Promise => { const [creatorResult, pluginsResult, templatesResult] = await Promise.allSettled([ getPublisher(uniqueHandle, publisherType), - getPublisherPlugins(uniqueHandle, sortField, sortOrder), - getPublisherTemplates(uniqueHandle, sortField, sortOrder), + fetchPublisherPluginPage({ uniqueHandle, page: 1, sortField, sortOrder }), + fetchPublisherTemplatePage({ uniqueHandle, page: 1, sortField, sortOrder }), ]) if (creatorResult.status === 'rejected') throw creatorResult.reason const creator = creatorResult.value if (!creator) return null - const plugins: MarketplacePlugin[] = - pluginsResult.status === 'fulfilled' ? pluginsResult.value : [] - const templates: MarketplaceTemplate[] = - templatesResult.status === 'fulfilled' ? templatesResult.value : [] + const plugins = pluginsResult.status === 'fulfilled' ? pluginsResult.value.items : [] + const templates = templatesResult.status === 'fulfilled' ? templatesResult.value.items : [] + const pluginPage = pluginsResult.status === 'fulfilled' ? pluginsResult.value : undefined + const templatePage = templatesResult.status === 'fulfilled' ? templatesResult.value : undefined const kind = publisherType === 'organization' ? 'organization' : 'individual' const resource = kind === 'organization' ? 'organizations' : 'creators' const encodedHandle = encodeURIComponent(uniqueHandle) @@ -160,21 +98,22 @@ const loadCreatorProfileCached = cache( resolveTemplateIcon: getTemplateIcon, resolveDependencyIcon: getDependencyIcon, }) + const records = toCreatorRecords({ locale, plugins, templates }) return { viewModel: { ...viewModel, creations: sortCreatorCreations(viewModel.creations, sortField, sortOrder), }, - pluginsByCreationId: Object.fromEntries( - plugins.map((plugin) => [ - `${plugin.type}:${plugin.org}/${plugin.name}`, - getFormattedPlugin(plugin), - ]), - ), - templatesByCreationId: Object.fromEntries( - templates.map((template) => [`template:${template.id}`, template]), - ), + pluginsByCreationId: records.pluginsByCreationId, + templatesByCreationId: records.templatesByCreationId, + inventory: { + uniqueHandle, + pluginHasMore: pluginPage?.hasMore ?? false, + templateHasMore: templatePage?.hasMore ?? false, + pluginNextPage: 2, + templateNextPage: 2, + }, } }, ) diff --git a/web/app/components/plugins/marketplace/creator-profile/dify-profile.tsx b/web/app/components/plugins/marketplace/creator-profile/dify-profile.tsx index 74f80bb3f27..8ae4997db63 100644 --- a/web/app/components/plugins/marketplace/creator-profile/dify-profile.tsx +++ b/web/app/components/plugins/marketplace/creator-profile/dify-profile.tsx @@ -34,7 +34,18 @@ const normalizePlugin = (plugin: Plugin): Plugin => ({ export default function DifyCreatorProfile({ loadedProfile, locale }: DifyCreatorProfileProps) { const router = useRouter() const [selected, setSelected] = useState(null) - const profilePlugins = Object.values(loadedProfile.pluginsByCreationId) + const [sourceProfile, setSourceProfile] = useState(loadedProfile) + const [pluginsByCreationId, setPluginsByCreationId] = useState(loadedProfile.pluginsByCreationId) + const [templatesByCreationId, setTemplatesByCreationId] = useState( + loadedProfile.templatesByCreationId, + ) + if (loadedProfile !== sourceProfile) { + setSourceProfile(loadedProfile) + setPluginsByCreationId(loadedProfile.pluginsByCreationId) + setTemplatesByCreationId(loadedProfile.templatesByCreationId) + } + + const profilePlugins = Object.values(pluginsByCreationId) const pluginIds = useMemo( () => Array.from( @@ -52,12 +63,12 @@ export default function DifyCreatorProfile({ loadedProfile, locale }: DifyCreato const selectCreation = (creation: CreatorCreation) => { if (creation.kind === 'plugin') { - const plugin = loadedProfile.pluginsByCreationId[creation.id] + const plugin = pluginsByCreationId[creation.id] if (plugin) setSelected({ kind: 'plugin', plugin: normalizePlugin(plugin) }) return } - const template = loadedProfile.templatesByCreationId[creation.id] + const template = templatesByCreationId[creation.id] if (template) setSelected({ kind: 'template', template }) } @@ -82,6 +93,15 @@ export default function DifyCreatorProfile({ loadedProfile, locale }: DifyCreato profile={loadedProfile.viewModel} homeHref="/marketplace" isMarketplacePlatform + inventory={loadedProfile.inventory} + locale={locale} + onRecordsLoaded={(records) => { + setPluginsByCreationId((current) => ({ ...current, ...records.pluginsByCreationId })) + setTemplatesByCreationId((current) => ({ + ...current, + ...records.templatesByCreationId, + })) + }} getCreationAction={(creation) => ({ type: 'select', onSelect: () => selectCreation(creation), diff --git a/web/app/components/plugins/marketplace/creator-profile/model.ts b/web/app/components/plugins/marketplace/creator-profile/model.ts index d76b770a8f1..9814dcec192 100644 --- a/web/app/components/plugins/marketplace/creator-profile/model.ts +++ b/web/app/components/plugins/marketplace/creator-profile/model.ts @@ -98,10 +98,19 @@ export type CreatorProfileViewModel = { creations: CreatorCreation[] } +export type CreatorInventory = { + uniqueHandle: string + pluginHasMore: boolean + templateHasMore: boolean + pluginNextPage: number + templateNextPage: number +} + export type LoadedCreatorProfile = { viewModel: CreatorProfileViewModel pluginsByCreationId: Record templatesByCreationId: Record + inventory: CreatorInventory } export type CreatorCreationAction = @@ -136,22 +145,26 @@ const toTimestamp = (value?: MarketplaceTimestamp | null) => { return Number.isNaN(timestamp) ? 0 : timestamp } +const firstLocalizedString = (value: object, keys: string[]) => { + for (const key of keys) { + const entry = (value as Record)[key] + if (typeof entry === 'string' && entry) return entry + } + return ( + Object.values(value).find((entry): entry is string => typeof entry === 'string' && !!entry) ?? + '' + ) +} + const getCreatorLocalizedText = ( value: Partial> | string | undefined, locale: string, ) => { if (typeof value === 'string') return value - if (!value) return '' + if (!value || typeof value !== 'object') return '' const normalizedLocale = locale.replace('-', '_') - return ( - value[locale] || - value[normalizedLocale] || - value['en-US'] || - value.en_US || - Object.values(value).find(Boolean) || - '' - ) + return firstLocalizedString(value, [locale, normalizedLocale, 'en-US', 'en_US']) } const getSocialPlatform = (hostname: string): CreatorSocialPlatform => { @@ -170,7 +183,8 @@ const getSocialPlatform = (hostname: string): CreatorSocialPlatform => { return 'website' } -export const normalizeCreatorSocialLink = (value: string): CreatorSocialLink | null => { +export const normalizeCreatorSocialLink = (value: unknown): CreatorSocialLink | null => { + if (typeof value !== 'string') return null const trimmedValue = value.trim() if (!trimmedValue) return null @@ -201,18 +215,22 @@ const getCreatorBadges = (creator: MarketplaceCreator) => { return Array.from(badges) } -export const adaptCreatorProfile = ({ - creator, - kind, +export const adaptCreations = ({ locale, - avatarUrl, - backgroundUrl, plugins, templates, resolvePluginIcon, resolveTemplateIcon, resolveDependencyIcon, -}: CreatorProfileAdapterInput): CreatorProfileViewModel => { +}: Pick< + CreatorProfileAdapterInput, + | 'locale' + | 'plugins' + | 'templates' + | 'resolvePluginIcon' + | 'resolveTemplateIcon' + | 'resolveDependencyIcon' +>): CreatorCreation[] => { const pluginCreations = plugins.map((plugin): CreatorCreation => ({ id: `${plugin.type}:${plugin.org}/${plugin.name}`, kind: 'plugin', @@ -240,7 +258,9 @@ export const adaptCreatorProfile = ({ const templateCreations = templates.map((template): CreatorCreation => { const templateIcon = resolveTemplateIcon(template) - const dependencyIds = template.deps_plugins ?? [] + const dependencyIds = (template.deps_plugins ?? []).filter( + (id): id is string => typeof id === 'string' && id.length > 0, + ) const publisher = template.publisher_handle || template.publisher_unique_handle || @@ -269,6 +289,21 @@ export const adaptCreatorProfile = ({ } }) + return [...pluginCreations, ...templateCreations] +} + +export const adaptCreatorProfile = ({ + creator, + kind, + locale, + avatarUrl, + backgroundUrl, + plugins, + templates, + resolvePluginIcon, + resolveTemplateIcon, + resolveDependencyIcon, +}: CreatorProfileAdapterInput): CreatorProfileViewModel => { return { profile: { kind, @@ -283,7 +318,14 @@ export const adaptCreatorProfile = ({ .map(normalizeCreatorSocialLink) .filter((link): link is CreatorSocialLink => link !== null), }, - creations: [...pluginCreations, ...templateCreations], + creations: adaptCreations({ + locale, + plugins, + templates, + resolvePluginIcon, + resolveTemplateIcon, + resolveDependencyIcon, + }), } } diff --git a/web/app/components/plugins/marketplace/creator-profile/publisher.ts b/web/app/components/plugins/marketplace/creator-profile/publisher.ts new file mode 100644 index 00000000000..ef0b6fdcbc9 --- /dev/null +++ b/web/app/components/plugins/marketplace/creator-profile/publisher.ts @@ -0,0 +1,102 @@ +import type { MarketplacePlugin, MarketplaceTemplate } from '@dify/contracts/marketplace' +import type { CreatorCreation, CreatorSortField, CreatorSortOrder } from './model' +import type { Plugin } from '@/app/components/plugins/types' +import { MARKETPLACE_API_PREFIX } from '@/config' +import { marketplaceClient } from '@/service/client' +import { getFormattedPlugin, getPluginIconInMarketplace } from '../utils' +import { adaptCreations, toPublisherSortQuery } from './model' + +export const CREATOR_PAGE_SIZE = 40 + +export type PublisherPage = { + items: T[] + total?: number + hasMore: boolean +} + +export const publisherPageHasMore = (page: number, itemCount: number, total?: number) => + typeof total === 'number' ? page * CREATOR_PAGE_SIZE < total : itemCount === CREATOR_PAGE_SIZE + +export const getTemplateIcon = (template: MarketplaceTemplate) => + template.icon_file_key + ? `${MARKETPLACE_API_PREFIX}/templates/${encodeURIComponent(template.id)}/icon` + : '' + +export const getDependencyIcon = (pluginId: string) => { + if (!pluginId.includes('/')) return '' + return `${MARKETPLACE_API_PREFIX}/plugins/${pluginId.split('/').map(encodeURIComponent).join('/')}/icon` +} + +export async function fetchPublisherPluginPage({ + uniqueHandle, + page, + sortField, + sortOrder, +}: { + uniqueHandle: string + page: number + sortField: CreatorSortField + sortOrder: CreatorSortOrder +}): Promise> { + const { plugins } = toPublisherSortQuery(sortField, sortOrder) + const response = await marketplaceClient.publisherPlugins({ + params: { uniqueHandle }, + query: { page, page_size: CREATOR_PAGE_SIZE, ...plugins }, + }) + const items = response.data?.plugins ?? [] + const total = response.data?.total + return { items, total, hasMore: publisherPageHasMore(page, items.length, total) } +} + +export async function fetchPublisherTemplatePage({ + uniqueHandle, + page, + sortField, + sortOrder, +}: { + uniqueHandle: string + page: number + sortField: CreatorSortField + sortOrder: CreatorSortOrder +}): Promise> { + const { templates } = toPublisherSortQuery(sortField, sortOrder) + const response = await marketplaceClient.publisherTemplates({ + params: { uniqueHandle }, + query: { page, page_size: CREATOR_PAGE_SIZE, ...templates }, + }) + const items = response.data?.templates ?? [] + const total = response.data?.total + return { items, total, hasMore: publisherPageHasMore(page, items.length, total) } +} + +export const toCreatorRecords = ({ + locale, + plugins, + templates, +}: { + locale: string + plugins: MarketplacePlugin[] + templates: MarketplaceTemplate[] +}): { + creations: CreatorCreation[] + pluginsByCreationId: Record + templatesByCreationId: Record +} => ({ + creations: adaptCreations({ + locale, + plugins, + templates, + resolvePluginIcon: getPluginIconInMarketplace, + resolveTemplateIcon: getTemplateIcon, + resolveDependencyIcon: getDependencyIcon, + }), + pluginsByCreationId: Object.fromEntries( + plugins.map((plugin) => [ + `${plugin.type}:${plugin.org}/${plugin.name}`, + getFormattedPlugin(plugin), + ]), + ), + templatesByCreationId: Object.fromEntries( + templates.map((template) => [`template:${template.id}`, template]), + ), +}) diff --git a/web/app/components/plugins/marketplace/creator-profile/view.tsx b/web/app/components/plugins/marketplace/creator-profile/view.tsx index ebae19c187e..fec3ac7e8d8 100644 --- a/web/app/components/plugins/marketplace/creator-profile/view.tsx +++ b/web/app/components/plugins/marketplace/creator-profile/view.tsx @@ -1,7 +1,14 @@ 'use client' +import type { MarketplaceTemplate } from '@dify/contracts/marketplace' import type { ReactNode } from 'react' -import type { CreatorCreation, CreatorCreationAction, CreatorProfileViewModel } from './model' +import type { + CreatorCreation, + CreatorCreationAction, + CreatorInventory, + CreatorProfileViewModel, +} from './model' +import type { Plugin } from '@/app/components/plugins/types' import { cn } from '@langgenius/dify-ui/cn' import { useTranslation } from '#i18n' import Link from '@/next/link' @@ -15,6 +22,12 @@ export type CreatorProfileViewProps = { header?: ReactNode homeHref: string isMarketplacePlatform: boolean + inventory?: CreatorInventory + locale?: string + onRecordsLoaded?: (records: { + pluginsByCreationId: Record + templatesByCreationId: Record + }) => void } export default function CreatorProfileView({ @@ -23,6 +36,9 @@ export default function CreatorProfileView({ header, homeHref, isMarketplacePlatform, + inventory, + locale, + onRecordsLoaded, }: CreatorProfileViewProps) { const { t } = useTranslation() @@ -79,7 +95,13 @@ export default function CreatorProfileView({ )} > - + diff --git a/web/i18n/ar-TN/plugin.json b/web/i18n/ar-TN/plugin.json index a7c37b5f10d..321f336910b 100644 --- a/web/i18n/ar-TN/plugin.json +++ b/web/i18n/ar-TN/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "الأعمال", "marketplace.creatorProfile.empty": "لا توجد أعمال بعد.", "marketplace.creatorProfile.home": "الصفحة الرئيسية للسوق", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "على الويب", "marketplace.creatorProfile.organization": "منظمة", "marketplace.creatorProfile.searchPlaceholder": "ابحث عن الإضافات والقوالب", diff --git a/web/i18n/de-DE/plugin.json b/web/i18n/de-DE/plugin.json index 4c3d321beff..b7afef2bda9 100644 --- a/web/i18n/de-DE/plugin.json +++ b/web/i18n/de-DE/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Kreationen", "marketplace.creatorProfile.empty": "Noch keine Kreationen.", "marketplace.creatorProfile.home": "Marketplace-Startseite", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "Im Web", "marketplace.creatorProfile.organization": "Organisation", "marketplace.creatorProfile.searchPlaceholder": "Plugins und Vorlagen suchen", diff --git a/web/i18n/en-US/plugin.json b/web/i18n/en-US/plugin.json index 18abf4dd904..b59877a0fdb 100644 --- a/web/i18n/en-US/plugin.json +++ b/web/i18n/en-US/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Creations", "marketplace.creatorProfile.empty": "No creations yet.", "marketplace.creatorProfile.home": "Marketplace home", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "On the web", "marketplace.creatorProfile.organization": "Organization", "marketplace.creatorProfile.searchPlaceholder": "Search plugins and templates", diff --git a/web/i18n/es-ES/plugin.json b/web/i18n/es-ES/plugin.json index e57b989c6a1..e4d07d3b7cf 100644 --- a/web/i18n/es-ES/plugin.json +++ b/web/i18n/es-ES/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Creaciones", "marketplace.creatorProfile.empty": "Aún no hay creaciones.", "marketplace.creatorProfile.home": "Inicio del Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "En la web", "marketplace.creatorProfile.organization": "Organización", "marketplace.creatorProfile.searchPlaceholder": "Buscar plugins y plantillas", diff --git a/web/i18n/fa-IR/plugin.json b/web/i18n/fa-IR/plugin.json index d1adac64969..4934bd863f0 100644 --- a/web/i18n/fa-IR/plugin.json +++ b/web/i18n/fa-IR/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "آثار", "marketplace.creatorProfile.empty": "هنوز اثری وجود ندارد.", "marketplace.creatorProfile.home": "خانه Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "در وب", "marketplace.creatorProfile.organization": "سازمان", "marketplace.creatorProfile.searchPlaceholder": "جستجوی افزونه و قالب", diff --git a/web/i18n/fr-FR/plugin.json b/web/i18n/fr-FR/plugin.json index 8cc5485fdff..f12125037a0 100644 --- a/web/i18n/fr-FR/plugin.json +++ b/web/i18n/fr-FR/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Créations", "marketplace.creatorProfile.empty": "Aucune création pour le moment.", "marketplace.creatorProfile.home": "Accueil du Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "Sur le web", "marketplace.creatorProfile.organization": "Organisation", "marketplace.creatorProfile.searchPlaceholder": "Rechercher des plugins et des modèles", diff --git a/web/i18n/hi-IN/plugin.json b/web/i18n/hi-IN/plugin.json index 4ec874c1712..8eecf5ef583 100644 --- a/web/i18n/hi-IN/plugin.json +++ b/web/i18n/hi-IN/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "रचनाएँ", "marketplace.creatorProfile.empty": "अभी कोई रचना नहीं।", "marketplace.creatorProfile.home": "Marketplace होम", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "वेब पर", "marketplace.creatorProfile.organization": "संगठन", "marketplace.creatorProfile.searchPlaceholder": "प्लगिन और टेम्पलेट खोजें", diff --git a/web/i18n/id-ID/plugin.json b/web/i18n/id-ID/plugin.json index 9c917d11807..17284d58c09 100644 --- a/web/i18n/id-ID/plugin.json +++ b/web/i18n/id-ID/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Karya", "marketplace.creatorProfile.empty": "Belum ada karya.", "marketplace.creatorProfile.home": "Beranda Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "Di web", "marketplace.creatorProfile.organization": "Organisasi", "marketplace.creatorProfile.searchPlaceholder": "Cari plugin dan template", diff --git a/web/i18n/it-IT/plugin.json b/web/i18n/it-IT/plugin.json index f3f2ff975a1..391b6627560 100644 --- a/web/i18n/it-IT/plugin.json +++ b/web/i18n/it-IT/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Creazioni", "marketplace.creatorProfile.empty": "Nessuna creazione al momento.", "marketplace.creatorProfile.home": "Home del Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "Sul web", "marketplace.creatorProfile.organization": "Organizzazione", "marketplace.creatorProfile.searchPlaceholder": "Cerca plugin e modelli", diff --git a/web/i18n/ja-JP/plugin.json b/web/i18n/ja-JP/plugin.json index 2b32e6b9708..00ce4ee68a1 100644 --- a/web/i18n/ja-JP/plugin.json +++ b/web/i18n/ja-JP/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "作品", "marketplace.creatorProfile.empty": "作品はまだありません。", "marketplace.creatorProfile.home": "Marketplace ホーム", + "marketplace.creatorProfile.loadMore": "さらに読み込む", + "marketplace.creatorProfile.loadMoreFailed": "作品を追加で読み込めませんでした。", "marketplace.creatorProfile.onTheWeb": "ウェブサイト", "marketplace.creatorProfile.organization": "組織", "marketplace.creatorProfile.searchPlaceholder": "プラグインとテンプレートを検索", diff --git a/web/i18n/ko-KR/plugin.json b/web/i18n/ko-KR/plugin.json index 3ce92d3cf39..8325c713d86 100644 --- a/web/i18n/ko-KR/plugin.json +++ b/web/i18n/ko-KR/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "작품", "marketplace.creatorProfile.empty": "아직 작품이 없습니다.", "marketplace.creatorProfile.home": "Marketplace 홈", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "웹에서", "marketplace.creatorProfile.organization": "조직", "marketplace.creatorProfile.searchPlaceholder": "플러그인 및 템플릿 검색", diff --git a/web/i18n/lo-LA/plugin.json b/web/i18n/lo-LA/plugin.json index 4e03771d312..6be5c041a7e 100644 --- a/web/i18n/lo-LA/plugin.json +++ b/web/i18n/lo-LA/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "ຜົນງານ", "marketplace.creatorProfile.empty": "ຍັງບໍ່ມີຜົນງານ.", "marketplace.creatorProfile.home": "ໜ້າຫຼັກ Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "ເທິງເວັບ", "marketplace.creatorProfile.organization": "ອົງກອນ", "marketplace.creatorProfile.searchPlaceholder": "ຄົ້ນຫາປລັກອິນ ແລະ ແມ່ແບບ", diff --git a/web/i18n/nl-NL/plugin.json b/web/i18n/nl-NL/plugin.json index ac5a406588a..ac1b6ce6966 100644 --- a/web/i18n/nl-NL/plugin.json +++ b/web/i18n/nl-NL/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Creaties", "marketplace.creatorProfile.empty": "Nog geen creaties.", "marketplace.creatorProfile.home": "Marketplace-startpagina", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "Op het web", "marketplace.creatorProfile.organization": "Organisatie", "marketplace.creatorProfile.searchPlaceholder": "Zoek plugins en sjablonen", diff --git a/web/i18n/pl-PL/plugin.json b/web/i18n/pl-PL/plugin.json index 31f7d70b715..1d1fa7eba20 100644 --- a/web/i18n/pl-PL/plugin.json +++ b/web/i18n/pl-PL/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Twórczość", "marketplace.creatorProfile.empty": "Brak prac.", "marketplace.creatorProfile.home": "Strona główna Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "W sieci", "marketplace.creatorProfile.organization": "Organizacja", "marketplace.creatorProfile.searchPlaceholder": "Szukaj wtyczek i szablonów", diff --git a/web/i18n/pt-BR/plugin.json b/web/i18n/pt-BR/plugin.json index da9710427db..78819acbfbf 100644 --- a/web/i18n/pt-BR/plugin.json +++ b/web/i18n/pt-BR/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Criações", "marketplace.creatorProfile.empty": "Nenhuma criação ainda.", "marketplace.creatorProfile.home": "Página inicial do Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "Na web", "marketplace.creatorProfile.organization": "Organização", "marketplace.creatorProfile.searchPlaceholder": "Pesquisar plugins e modelos", diff --git a/web/i18n/ro-RO/plugin.json b/web/i18n/ro-RO/plugin.json index a02da38423e..5f760875155 100644 --- a/web/i18n/ro-RO/plugin.json +++ b/web/i18n/ro-RO/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Creații", "marketplace.creatorProfile.empty": "Nicio creație încă.", "marketplace.creatorProfile.home": "Pagina principală Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "Pe web", "marketplace.creatorProfile.organization": "Organizație", "marketplace.creatorProfile.searchPlaceholder": "Caută pluginuri și șabloane", diff --git a/web/i18n/ru-RU/plugin.json b/web/i18n/ru-RU/plugin.json index 3b886c0ff4b..923af28c8b5 100644 --- a/web/i18n/ru-RU/plugin.json +++ b/web/i18n/ru-RU/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Работы", "marketplace.creatorProfile.empty": "Пока нет работ.", "marketplace.creatorProfile.home": "Главная Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "В интернете", "marketplace.creatorProfile.organization": "Организация", "marketplace.creatorProfile.searchPlaceholder": "Поиск плагинов и шаблонов", diff --git a/web/i18n/sl-SI/plugin.json b/web/i18n/sl-SI/plugin.json index 959425d379f..00044dcba9a 100644 --- a/web/i18n/sl-SI/plugin.json +++ b/web/i18n/sl-SI/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Stvaritve", "marketplace.creatorProfile.empty": "Še ni stvaritev.", "marketplace.creatorProfile.home": "Domov Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "Na spletu", "marketplace.creatorProfile.organization": "Organizacija", "marketplace.creatorProfile.searchPlaceholder": "Iskanje vtičnikov in predlog", diff --git a/web/i18n/th-TH/plugin.json b/web/i18n/th-TH/plugin.json index 67b64c4ded7..537b24e3170 100644 --- a/web/i18n/th-TH/plugin.json +++ b/web/i18n/th-TH/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "ผลงาน", "marketplace.creatorProfile.empty": "ยังไม่มีผลงาน", "marketplace.creatorProfile.home": "หน้าแรก Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "บนเว็บ", "marketplace.creatorProfile.organization": "องค์กร", "marketplace.creatorProfile.searchPlaceholder": "ค้นหาปลั๊กอินและเทมเพลต", diff --git a/web/i18n/tr-TR/plugin.json b/web/i18n/tr-TR/plugin.json index 2718093eb6e..01b69d79f78 100644 --- a/web/i18n/tr-TR/plugin.json +++ b/web/i18n/tr-TR/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Çalışmalar", "marketplace.creatorProfile.empty": "Henüz çalışma yok.", "marketplace.creatorProfile.home": "Marketplace ana sayfası", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "Web'de", "marketplace.creatorProfile.organization": "Organizasyon", "marketplace.creatorProfile.searchPlaceholder": "Eklenti ve şablon ara", diff --git a/web/i18n/uk-UA/plugin.json b/web/i18n/uk-UA/plugin.json index 80dff6cdd78..74c0276ad99 100644 --- a/web/i18n/uk-UA/plugin.json +++ b/web/i18n/uk-UA/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Роботи", "marketplace.creatorProfile.empty": "Поки немає робіт.", "marketplace.creatorProfile.home": "Головна Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "В інтернеті", "marketplace.creatorProfile.organization": "Організація", "marketplace.creatorProfile.searchPlaceholder": "Пошук плагінів і шаблонів", diff --git a/web/i18n/vi-VN/plugin.json b/web/i18n/vi-VN/plugin.json index 1967395535d..e203765e754 100644 --- a/web/i18n/vi-VN/plugin.json +++ b/web/i18n/vi-VN/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "Tác phẩm", "marketplace.creatorProfile.empty": "Chưa có tác phẩm nào.", "marketplace.creatorProfile.home": "Trang chủ Marketplace", + "marketplace.creatorProfile.loadMore": "Load more", + "marketplace.creatorProfile.loadMoreFailed": "Couldn't load more creations.", "marketplace.creatorProfile.onTheWeb": "Trên web", "marketplace.creatorProfile.organization": "Tổ chức", "marketplace.creatorProfile.searchPlaceholder": "Tìm plugin và mẫu", diff --git a/web/i18n/zh-Hans/plugin.json b/web/i18n/zh-Hans/plugin.json index efae41eb2a8..306a946e2de 100644 --- a/web/i18n/zh-Hans/plugin.json +++ b/web/i18n/zh-Hans/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "作品", "marketplace.creatorProfile.empty": "暂无作品。", "marketplace.creatorProfile.home": "Marketplace 首页", + "marketplace.creatorProfile.loadMore": "加载更多", + "marketplace.creatorProfile.loadMoreFailed": "无法加载更多作品。", "marketplace.creatorProfile.onTheWeb": "社交主页", "marketplace.creatorProfile.organization": "组织", "marketplace.creatorProfile.searchPlaceholder": "搜索插件和模板", diff --git a/web/i18n/zh-Hant/plugin.json b/web/i18n/zh-Hant/plugin.json index e22a867d9e8..4d1eabd3e18 100644 --- a/web/i18n/zh-Hant/plugin.json +++ b/web/i18n/zh-Hant/plugin.json @@ -233,6 +233,8 @@ "marketplace.creatorProfile.creations": "作品", "marketplace.creatorProfile.empty": "尚無作品。", "marketplace.creatorProfile.home": "Marketplace 首頁", + "marketplace.creatorProfile.loadMore": "載入更多", + "marketplace.creatorProfile.loadMoreFailed": "無法載入更多作品。", "marketplace.creatorProfile.onTheWeb": "社交主頁", "marketplace.creatorProfile.organization": "組織", "marketplace.creatorProfile.searchPlaceholder": "搜尋外掛和模板",