diff --git a/web/__tests__/apps/app-list-browsing-flow.test.tsx b/web/__tests__/apps/app-list-browsing-flow.test.tsx index 555702a66f9..59e53051e11 100644 --- a/web/__tests__/apps/app-list-browsing-flow.test.tsx +++ b/web/__tests__/apps/app-list-browsing-flow.test.tsx @@ -129,6 +129,7 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { }) vi.mock('@/service/use-apps', () => ({ + normalizeAppPagination: (response: T) => response, useDeleteAppMutation: () => ({ mutateAsync: vi.fn(), isPending: false, diff --git a/web/__tests__/apps/create-app-flow.test.tsx b/web/__tests__/apps/create-app-flow.test.tsx index 237dec91230..4900593d505 100644 --- a/web/__tests__/apps/create-app-flow.test.tsx +++ b/web/__tests__/apps/create-app-flow.test.tsx @@ -104,6 +104,7 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { }) vi.mock('@/service/use-apps', () => ({ + normalizeAppPagination: (response: T) => response, useDeleteAppMutation: () => ({ mutateAsync: vi.fn(), isPending: false, diff --git a/web/__tests__/explore/explore-app-list-flow.test.tsx b/web/__tests__/explore/explore-app-list-flow.test.tsx index 786fef89486..a5a4971daee 100644 --- a/web/__tests__/explore/explore-app-list-flow.test.tsx +++ b/web/__tests__/explore/explore-app-list-flow.test.tsx @@ -76,7 +76,7 @@ vi.mock('@/service/client', () => ({ }, }, apps: { - list: { + get: { queryOptions: (options: { input?: { query?: { limit?: number } } select?: (response: { @@ -96,7 +96,7 @@ vi.mock('@/service/client', () => ({ total: 0, } return { - queryKey: ['console', 'apps', 'list', options.input], + queryKey: ['console', 'apps', 'get', options.input], queryFn: () => Promise.resolve(response), initialData: response, select: options.select, @@ -217,7 +217,7 @@ const homeContinueWorkAppsInput = { const createHomeQueryClient = () => { const queryClient = createTestQueryClient() - queryClient.setQueryData(['console', 'apps', 'list', homeContinueWorkAppsInput], { + queryClient.setQueryData(['console', 'apps', 'get', homeContinueWorkAppsInput], { data: [], has_more: false, limit: 8, diff --git a/web/__tests__/unified-tags-logic.test.ts b/web/__tests__/unified-tags-logic.test.ts index d4bbd7f4ff0..25324b65838 100644 --- a/web/__tests__/unified-tags-logic.test.ts +++ b/web/__tests__/unified-tags-logic.test.ts @@ -251,7 +251,7 @@ describe('Unified Tags Editing - Pure Logic Tests', () => { id: 'tag1', name: 'test-tag', type: 'app', - binding_count: 1, + binding_count: '', } expect(tag).toHaveProperty('id') @@ -259,13 +259,13 @@ describe('Unified Tags Editing - Pure Logic Tests', () => { expect(tag).toHaveProperty('type') expect(tag).toHaveProperty('binding_count') expect(tag.type).toBe('app') - expect(typeof tag.binding_count).toBe('number') + expect(typeof tag.binding_count).toBe('string') }) it('should handle tag arrays correctly', () => { const tags = [ - { id: 'tag1', name: 'Tag 1', type: 'app', binding_count: 1 }, - { id: 'tag2', name: 'Tag 2', type: 'app', binding_count: 0 }, + { id: 'tag1', name: 'Tag 1', type: 'app', binding_count: '' }, + { id: 'tag2', name: 'Tag 2', type: 'app', binding_count: '' }, ] expect(Array.isArray(tags)).toBe(true) @@ -278,7 +278,7 @@ describe('Unified Tags Editing - Pure Logic Tests', () => { id: 'test-app', name: 'Test App', tags: [ - { id: 'tag1', name: 'Tag 1', type: 'app', binding_count: 1 }, + { id: 'tag1', name: 'Tag 1', type: 'app', binding_count: '' }, ], } @@ -308,22 +308,22 @@ describe('Unified Tags Editing - Pure Logic Tests', () => { it('should handle malformed tag data gracefully', () => { const mixedData = [ - { id: 'valid1', name: 'Valid Tag', type: 'app', binding_count: 1 }, + { id: 'valid1', name: 'Valid Tag', type: 'app', binding_count: '' }, { id: 'invalid1' }, // Missing required properties null, undefined, - { id: 'valid2', name: 'Another Valid', type: 'app', binding_count: 0 }, + { id: 'valid2', name: 'Another Valid', type: 'app', binding_count: '' }, ] // Filter out invalid entries - const validTags = mixedData.filter((tag): tag is { id: string, name: string, type: string, binding_count: number } => + const validTags = mixedData.filter((tag): tag is { id: string, name: string, type: string, binding_count: string } => tag != null && typeof tag === 'object' && 'id' in tag && 'name' in tag && 'type' in tag && 'binding_count' in tag - && typeof tag.binding_count === 'number', + && typeof tag.binding_count === 'string', ) expect(validTags.length).toBe(2) diff --git a/web/app/account/(commonLayout)/account-page/index.tsx b/web/app/account/(commonLayout)/account-page/index.tsx index d215328acfb..70bcf7c7922 100644 --- a/web/app/account/(commonLayout)/account-page/index.tsx +++ b/web/app/account/(commonLayout)/account-page/index.tsx @@ -20,6 +20,7 @@ import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { consoleQuery } from '@/service/client' import { updateUserProfile } from '@/service/common' +import { normalizeAppPagination } from '@/service/use-apps' import DeleteAccount from '../delete-account' import AvatarWithEdit from './AvatarWithEdit' @@ -35,7 +36,7 @@ const descriptionClassName = ` export default function AccountPage() { const { t } = useTranslation() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) - const { data: appList } = useQuery(consoleQuery.apps.list.queryOptions({ + const { data: appList } = useQuery(consoleQuery.apps.get.queryOptions({ input: { query: { page: 1, @@ -43,6 +44,7 @@ export default function AccountPage() { name: '', }, }, + select: normalizeAppPagination, })) const apps = appList?.data || [] const queryClient = useQueryClient() diff --git a/web/app/components/app/in-site-message/__tests__/notification.spec.tsx b/web/app/components/app/in-site-message/__tests__/notification.spec.tsx index 0af00256789..028b2538152 100644 --- a/web/app/components/app/in-site-message/__tests__/notification.spec.tsx +++ b/web/app/components/app/in-site-message/__tests__/notification.spec.tsx @@ -29,18 +29,22 @@ vi.mock(import('@/config'), async (importOriginal) => { vi.mock('@/service/client', () => ({ consoleQuery: { notification: { - queryOptions: (options?: Record) => ({ - queryKey: ['console', 'notification'], - queryFn: (...args: unknown[]) => mockNotification(...args), - ...options, - }), - }, - notificationDismiss: { - mutationOptions: (options?: Record) => ({ - mutationKey: ['console', 'notificationDismiss'], - mutationFn: (...args: unknown[]) => mockNotificationDismiss(...args), - ...options, - }), + get: { + queryOptions: (options?: Record) => ({ + queryKey: ['console', 'notification', 'get'], + queryFn: (...args: unknown[]) => mockNotification(...args), + ...options, + }), + }, + dismiss: { + post: { + mutationOptions: (options?: Record) => ({ + mutationKey: ['console', 'notification', 'dismiss', 'post'], + mutationFn: (...args: unknown[]) => mockNotificationDismiss(...args), + ...options, + }), + }, + }, }, }, })) @@ -149,7 +153,7 @@ describe('InSiteMessageNotification', () => { }, }, expect.objectContaining({ - mutationKey: ['console', 'notificationDismiss'], + mutationKey: ['console', 'notification', 'dismiss', 'post'], }), ) }) @@ -187,7 +191,7 @@ describe('InSiteMessageNotification', () => { }, }, expect.objectContaining({ - mutationKey: ['console', 'notificationDismiss'], + mutationKey: ['console', 'notification', 'dismiss', 'post'], }), ) }) diff --git a/web/app/components/app/in-site-message/notification.tsx b/web/app/components/app/in-site-message/notification.tsx index 97ac168cb5d..ea8cf0bda04 100644 --- a/web/app/components/app/in-site-message/notification.tsx +++ b/web/app/components/app/in-site-message/notification.tsx @@ -60,18 +60,19 @@ function parseNotificationBody(body: string): NotificationBodyPayload | null { function InSiteMessageNotification() { const { t } = useTranslation() - const dismissNotificationMutation = useMutation(consoleQuery.notificationDismiss.mutationOptions()) + const dismissNotificationMutation = useMutation(consoleQuery.notification.dismiss.post.mutationOptions()) - const { data } = useQuery(consoleQuery.notification.queryOptions({ + const { data } = useQuery(consoleQuery.notification.get.queryOptions({ enabled: IS_CLOUD_EDITION, })) const notification = data?.notifications?.[0] const parsedBody = notification ? parseNotificationBody(notification.body) : null - if (!IS_CLOUD_EDITION || !notification) + if (!IS_CLOUD_EDITION || !notification || !notification.notification_id) return null + const notificationId = notification.notification_id const fallbackActions: InSiteMessageActionItem[] = [ { type: 'default', @@ -89,15 +90,15 @@ function InSiteMessageNotification() { dismissNotificationMutation.mutate({ body: { - notification_id: notification.notification_id, + notification_id: notificationId, }, }) } return ( { author_name: 'Readonly Author', created_by: 'another-user', maintainer: 'another-user', - tags: [{ id: 'tag-preview', name: 'Readonly Tag', type: 'app' as const, binding_count: 0 }], + tags: [{ id: 'tag-preview', name: 'Readonly Tag', type: 'app' as const, binding_count: '' }], permission_keys: [AppACLPermission.Preview], }) @@ -501,7 +501,7 @@ describe('AppCard', () => { it('should handle app with tags', () => { const appWithTags = { ...mockApp, - tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: 0 }], + tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: '' }], } render() // Verify the tag selector component renders @@ -510,10 +510,10 @@ describe('AppCard', () => { it('should display refreshed tag names from app props when tag ids stay the same', () => { const firstApp = createMockApp({ - tags: [{ id: 'tag1', name: 'Old Tag', type: 'app' as const, binding_count: 0 }], + tags: [{ id: 'tag1', name: 'Old Tag', type: 'app' as const, binding_count: '' }], }) const refreshedApp = createMockApp({ - tags: [{ id: 'tag1', name: 'New Tag', type: 'app' as const, binding_count: 0 }], + tags: [{ id: 'tag1', name: 'New Tag', type: 'app' as const, binding_count: '' }], }) const { rerender } = render() @@ -531,7 +531,7 @@ describe('AppCard', () => { mockAppContext.userProfile = { id: 'user-2' } const editableApp = createMockApp({ maintainer: 'user-1', - tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: 0 }], + tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: '' }], permission_keys: [AppACLPermission.Edit], }) @@ -546,7 +546,7 @@ describe('AppCard', () => { mockAppContext.userProfile = { id: 'user-2' } const tagManageApp = createMockApp({ maintainer: 'user-1', - tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: 0 }], + tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: '' }], permission_keys: [AppACLPermission.ViewLayout], }) @@ -561,7 +561,7 @@ describe('AppCard', () => { mockAppContext.userProfile = { id: 'user-2' } const readonlyApp = createMockApp({ maintainer: 'user-1', - tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: 0 }], + tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: '' }], permission_keys: [AppACLPermission.ViewLayout], }) @@ -1429,9 +1429,9 @@ describe('AppCard', () => { const multiTagApp = { ...mockApp, tags: [ - { id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: 0 }, - { id: 'tag2', name: 'Tag 2', type: 'app' as const, binding_count: 0 }, - { id: 'tag3', name: 'Tag 3', type: 'app' as const, binding_count: 0 }, + { id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: '' }, + { id: 'tag2', name: 'Tag 2', type: 'app' as const, binding_count: '' }, + { id: 'tag3', name: 'Tag 3', type: 'app' as const, binding_count: '' }, ], } render() @@ -1586,7 +1586,7 @@ describe('AppCard', () => { it('should stop propagation when clicking tag selector area', () => { const multiTagApp = createMockApp({ - tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: 0 }], + tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: '' }], }) render() diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index 8830f32ee31..e2d8056ba7b 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -46,15 +46,17 @@ vi.mock('@/service/client', () => ({ }, consoleQuery: { apps: { - list: { + get: { infiniteOptions: (options: unknown) => mockAppListInfiniteOptions(options), }, - starredList: { - queryOptions: (options: unknown) => mockAppStarredListQueryOptions(options), + starred: { + get: { + queryOptions: (options: unknown) => mockAppStarredListQueryOptions(options), + }, }, }, tags: { - list: { + get: { queryOptions: (options: unknown) => options, }, }, @@ -229,6 +231,7 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { }) vi.mock('@/service/use-apps', () => ({ + normalizeAppPagination: (response: unknown) => response, useDeleteAppMutation: () => ({ mutateAsync: vi.fn(), isPending: false, diff --git a/web/app/components/apps/app-list-header-filters.tsx b/web/app/components/apps/app-list-header-filters.tsx index df32f57d7ba..04379fd5c1d 100644 --- a/web/app/components/apps/app-list-header-filters.tsx +++ b/web/app/components/apps/app-list-header-filters.tsx @@ -1,7 +1,7 @@ 'use client' +import type { GetAppsData } from '@dify/contracts/api/console/apps/types.gen' import type { AppListCategory } from './app-type-filter-shared' -import type { AppListSortBy } from '@/contract/console/apps' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu' @@ -13,6 +13,9 @@ import { AppSortFilter } from './app-sort-filter' import { AppTypeFilter } from './app-type-filter' import CreatorsFilter from './creators-filter' +type AppListQuery = NonNullable +type AppListSortBy = NonNullable + type AppListHeaderFiltersProps = { category: AppListCategory tagIDs: string[] diff --git a/web/app/components/apps/app-sort-filter.tsx b/web/app/components/apps/app-sort-filter.tsx index 60555bec136..ef67d2571da 100644 --- a/web/app/components/apps/app-sort-filter.tsx +++ b/web/app/components/apps/app-sort-filter.tsx @@ -1,6 +1,6 @@ 'use client' -import type { AppListSortBy } from '@/contract/console/apps' +import type { GetAppsData } from '@dify/contracts/api/console/apps/types.gen' import { DropdownMenu, DropdownMenuContent, @@ -12,6 +12,9 @@ import { import { useMemo } from 'react' import { useTranslation } from 'react-i18next' +type AppListQuery = NonNullable +type AppListSortBy = NonNullable + type AppSortFilterProps = { value: AppListSortBy onChange: (value: AppListSortBy) => void diff --git a/web/app/components/apps/hooks/__tests__/use-workflow-online-users.spec.ts b/web/app/components/apps/hooks/__tests__/use-workflow-online-users.spec.ts index bdaefc67ba1..5f9c9581172 100644 --- a/web/app/components/apps/hooks/__tests__/use-workflow-online-users.spec.ts +++ b/web/app/components/apps/hooks/__tests__/use-workflow-online-users.spec.ts @@ -25,8 +25,12 @@ vi.mock('@tanstack/react-query', () => ({ vi.mock('@/service/client', () => ({ consoleQuery: { apps: { - workflowOnlineUsers: { - queryOptions: mockQueryOptions, + workflows: { + onlineUsers: { + post: { + queryOptions: mockQueryOptions, + }, + }, }, }, }, @@ -35,7 +39,7 @@ vi.mock('@/service/client', () => ({ const getLastQueryOptions = () => { const lastCall = mockQueryOptions.mock.lastCall if (!lastCall) - throw new Error('workflowOnlineUsers.queryOptions was not called') + throw new Error('workflows.onlineUsers.post.queryOptions was not called') return lastCall[0] as QueryOptions } diff --git a/web/app/components/apps/hooks/use-workflow-online-users.ts b/web/app/components/apps/hooks/use-workflow-online-users.ts index a0ec6a2d4e7..919e22785cf 100644 --- a/web/app/components/apps/hooks/use-workflow-online-users.ts +++ b/web/app/components/apps/hooks/use-workflow-online-users.ts @@ -35,7 +35,7 @@ export const useWorkflowOnlineUsers = ({ enabled, }: UseWorkflowOnlineUsersParams) => { const shouldFetch = enabled && appIds.length > 0 - const { data: onlineUsersMap = {} } = useQuery(consoleQuery.apps.workflowOnlineUsers.queryOptions({ + const { data: onlineUsersMap = {} } = useQuery(consoleQuery.apps.workflows.onlineUsers.post.queryOptions({ input: { body: { app_ids: appIds } }, enabled: shouldFetch, select: normalizeWorkflowOnlineUsers, diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index 20a197e8ffe..7287606ae68 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -1,6 +1,6 @@ 'use client' -import type { AppListQuery, AppListSortBy } from '@/contract/console/apps' +import type { GetAppsData } from '@dify/contracts/api/console/apps/types.gen' import { cn } from '@langgenius/dify-ui/cn' import { keepPreviousData, useInfiniteQuery, useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useDebounce } from 'ahooks' @@ -13,6 +13,7 @@ import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { CheckModal } from '@/hooks/use-pay' import { usePathname, useRouter, useSearchParams } from '@/next/navigation' import { consoleQuery } from '@/service/client' +import { normalizeAppPagination } from '@/service/use-apps' import { AppModeEnum } from '@/types/app' import { hasPermission } from '@/utils/permission' import { AppCard } from './app-card' @@ -31,6 +32,9 @@ import { StudioListHeader } from './studio-list-header' const STARRED_APP_LIMIT = 100 +type AppListQuery = NonNullable +type AppListSortBy = NonNullable + type Props = Readonly<{ controlRefreshList?: number }> @@ -109,7 +113,7 @@ function List({ error, refetch, } = useInfiniteQuery({ - ...consoleQuery.apps.list.infiniteOptions({ + ...consoleQuery.apps.get.infiniteOptions({ input: pageParam => ({ query: { ...appListQuery, @@ -120,6 +124,10 @@ function List({ initialPageParam: 1, placeholderData: keepPreviousData, }), + select: data => ({ + ...data, + pages: data.pages.map(normalizeAppPagination), + }), refetchInterval: systemFeatures.enable_collaboration_mode ? 10000 : false, }) @@ -133,10 +141,11 @@ function List({ data: starredAppList, refetch: refetchStarredAppList, } = useQuery({ - ...consoleQuery.apps.starredList.queryOptions({ + ...consoleQuery.apps.starred.get.queryOptions({ input: { query: starredAppListQuery, }, + select: normalizeAppPagination, }), }) diff --git a/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx b/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx index 46fe308a720..bbe58038d6e 100644 --- a/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx @@ -300,7 +300,7 @@ describe('DatasetCard Component', () => { const dataset = createMockDataset({ name: 'Preview Only Dataset', permission_keys: [DatasetACLPermission.Preview], - tags: [{ id: 'tag-preview', name: 'Readonly Tag', type: 'knowledge' as const, binding_count: 0 }], + tags: [{ id: 'tag-preview', name: 'Readonly Tag', type: 'knowledge' as const, binding_count: '' }], }) render() diff --git a/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts b/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts index 6229fafac2c..e4a7bea9bd2 100644 --- a/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts +++ b/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts @@ -52,7 +52,7 @@ describe('useDatasetCardState', () => { word_count: 1000, created_at: 1609459200, updated_at: 1609545600, - tags: [{ id: 'tag-1', name: 'Tag 1', type: 'knowledge', binding_count: 0 }], + tags: [{ id: 'tag-1', name: 'Tag 1', type: 'knowledge', binding_count: '' }], embedding_model: 'text-embedding-ada-002', embedding_model_provider: 'openai', created_by: 'user-1', diff --git a/web/app/components/explore/app-list/__tests__/index.spec.tsx b/web/app/components/explore/app-list/__tests__/index.spec.tsx index f42e34af994..76813797424 100644 --- a/web/app/components/explore/app-list/__tests__/index.spec.tsx +++ b/web/app/components/explore/app-list/__tests__/index.spec.tsx @@ -77,7 +77,7 @@ vi.mock('@/service/client', () => ({ }, }, apps: { - list: { + get: { queryOptions: (options: { input?: { query?: { limit?: number } } select?: (response: { @@ -91,7 +91,7 @@ vi.mock('@/service/client', () => ({ const limit = options.input?.query?.limit ?? mockWorkspaceApps.length if (mockWorkspaceAppsLoading) { return { - queryKey: ['console', 'apps', 'list', options], + queryKey: ['console', 'apps', 'get', options], queryFn: () => new Promise(() => {}), select: options.select, } @@ -104,7 +104,7 @@ vi.mock('@/service/client', () => ({ total: mockWorkspaceApps.length, } return { - queryKey: ['console', 'apps', 'list', options], + queryKey: ['console', 'apps', 'get', options], queryFn: () => Promise.resolve(response), initialData: response, select: options.select, diff --git a/web/app/components/explore/app-list/index.tsx b/web/app/components/explore/app-list/index.tsx index feb253173e7..65b437d75bb 100644 --- a/web/app/components/explore/app-list/index.tsx +++ b/web/app/components/explore/app-list/index.tsx @@ -25,6 +25,7 @@ import { DSLImportMode } from '@/models/app' import dynamic from '@/next/dynamic' import { consoleQuery } from '@/service/client' import { fetchAppDetail, fetchAppList, fetchBanners } from '@/service/explore' +import { normalizeAppPagination } from '@/service/use-apps' import { trackCreateApp } from '@/utils/create-app-tracking' import { hasPermission } from '@/utils/permission' import { ExploreAppListHeader } from './explore-app-list-header' @@ -72,9 +73,9 @@ function getExploreAppListQueryOptions(locale?: string) { } function getContinueWorkAppsQueryOptions() { - return consoleQuery.apps.list.queryOptions({ + return consoleQuery.apps.get.queryOptions({ input: homeContinueWorkAppsInput, - select: (response): WorkspaceApp[] => response.data ?? [], + select: (response): WorkspaceApp[] => normalizeAppPagination(response).data, }) } diff --git a/web/app/components/header/account-dropdown/workplace-selector/index.tsx b/web/app/components/header/account-dropdown/workplace-selector/index.tsx index 8e11d5ce271..22171f15213 100644 --- a/web/app/components/header/account-dropdown/workplace-selector/index.tsx +++ b/web/app/components/header/account-dropdown/workplace-selector/index.tsx @@ -1,5 +1,4 @@ -import type { Plan } from '@/app/components/billing/type' -import type { IWorkspace } from '@/models/common' +import type { TenantListItemResponse } from '@dify/contracts/api/console/workspaces/types.gen' import { SelectContent, SelectGroup, @@ -9,30 +8,42 @@ import { } from '@langgenius/dify-ui/select' import { memo } from 'react' import { useTranslation } from 'react-i18next' +import { Plan } from '@/app/components/billing/type' import { PlanBadge } from '@/app/components/header/plan-badge' type WorkplaceSelectorContentProps = { - workspaces: IWorkspace[] + workspaces: TenantListItemResponse[] popupClassName?: string } type WorkplaceSelectorItemProps = { - workspace: IWorkspace + workspace: TenantListItemResponse +} + +const workspacePlans = new Set(Object.values(Plan)) + +function isWorkspacePlan(plan: string | null | undefined): plan is Plan { + return !!plan && workspacePlans.has(plan) } const WorkplaceSelectorItem = memo(({ workspace, -}: WorkplaceSelectorItemProps) => ( - -
- - {workspace.name[0]?.toLocaleUpperCase()} - -
- {workspace.name} - -
-)) +}: WorkplaceSelectorItemProps) => { + const workspaceName = workspace.name || workspace.id + const workspacePlan = isWorkspacePlan(workspace.plan) ? workspace.plan : Plan.sandbox + + return ( + +
+ + {workspaceName[0]?.toLocaleUpperCase()} + +
+ {workspaceName} + +
+ ) +}) WorkplaceSelectorItem.displayName = 'WorkplaceSelectorItem' export const WorkplaceSelectorContent = memo(({ diff --git a/web/app/components/main-nav/components/workspace-card.tsx b/web/app/components/main-nav/components/workspace-card.tsx index 9a8dea0280c..26c9582a2a1 100644 --- a/web/app/components/main-nav/components/workspace-card.tsx +++ b/web/app/components/main-nav/components/workspace-card.tsx @@ -34,8 +34,8 @@ const workspaceMenuAlignOffset = -28 const workspaceCardSkeletonClassName = 'animate-pulse rounded bg-text-quaternary opacity-20 motion-reduce:animate-none' const workspacePlans = new Set(Object.values(Plan)) -function isWorkspacePlan(plan: string): plan is Plan { - return workspacePlans.has(plan) +function isWorkspacePlan(plan: string | null | undefined): plan is Plan { + return !!plan && workspacePlans.has(plan) } function WorkspaceCardSkeleton({ diff --git a/web/app/components/main-nav/components/workspace-switcher.tsx b/web/app/components/main-nav/components/workspace-switcher.tsx index 44919b152e9..97041d68e4d 100644 --- a/web/app/components/main-nav/components/workspace-switcher.tsx +++ b/web/app/components/main-nav/components/workspace-switcher.tsx @@ -1,6 +1,6 @@ 'use client' -import type { IWorkspace } from '@/models/common' +import type { TenantListItemResponse } from '@dify/contracts/api/console/workspaces/types.gen' import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, @@ -21,8 +21,13 @@ const workspaceSwitchActionIconClassName = 'size-3.5 shrink-0' const workspaceSwitchListClassName = 'max-h-[240px] overflow-y-auto overscroll-contain scroll-py-1' const workspaceSwitchI18nKey = (key: string) => key as 'mainNav.workspace.settings' type WorkspaceSort = 'lastOpened' | 'createdAt' +type WorkspaceListItem = TenantListItemResponse & { + last_opened_at?: number | null +} -const getWorkspaceLastOpenedAt = (workspace: IWorkspace) => workspace.last_opened_at ?? 0 +const getWorkspaceName = (workspace: WorkspaceListItem) => workspace.name || workspace.id +const getWorkspaceCreatedAt = (workspace: WorkspaceListItem) => workspace.created_at ?? 0 +const getWorkspaceLastOpenedAt = (workspace: WorkspaceListItem) => workspace.last_opened_at ?? 0 function WorkspaceSwitchControls({ searchText, @@ -118,7 +123,7 @@ function WorkspaceSwitchControls({ } type WorkspaceSwitcherProps = { - workspaces: IWorkspace[] + workspaces: WorkspaceListItem[] onSwitchWorkspace: (workspaceId: string) => void } @@ -131,15 +136,15 @@ export function WorkspaceSwitcher({ const displayedWorkspaces = useMemo(() => { const normalizedSearchText = workspaceSearchText.trim().toLowerCase() const filteredWorkspaces = normalizedSearchText - ? workspaces.filter(workspace => workspace.name.toLowerCase().includes(normalizedSearchText)) + ? workspaces.filter(workspace => getWorkspaceName(workspace).toLowerCase().includes(normalizedSearchText)) : [...workspaces] if (workspaceSort === 'createdAt') - return filteredWorkspaces.sort((a, b) => b.created_at - a.created_at) + return filteredWorkspaces.sort((a, b) => getWorkspaceCreatedAt(b) - getWorkspaceCreatedAt(a)) return filteredWorkspaces.sort((a, b) => { return getWorkspaceLastOpenedAt(b) - getWorkspaceLastOpenedAt(a) - || b.created_at - a.created_at + || getWorkspaceCreatedAt(b) - getWorkspaceCreatedAt(a) }) }, [workspaceSearchText, workspaceSort, workspaces]) @@ -152,27 +157,31 @@ export function WorkspaceSwitcher({ onSortChange={setWorkspaceSort} />
- {displayedWorkspaces.map(workspace => ( - - ))} + {displayedWorkspaces.map((workspace) => { + const workspaceName = getWorkspaceName(workspace) + + return ( + + ) + })}
) diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx index 8810d35b280..e2423ed7a71 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx @@ -34,7 +34,7 @@ const apps: App[] = [ vi.mock('@/service/client', () => ({ consoleQuery: { apps: { - list: { + get: { infiniteOptions: ({ input, getNextPageParam, @@ -70,6 +70,7 @@ vi.mock('@/service/client', () => ({ })) vi.mock('@/service/use-apps', () => ({ + normalizeAppPagination: (response: T) => response, useAppDetail: (appId: string) => ({ data: apps.find(app => app.id === appId), }), diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx index 8d3094a2a0f..f03b47bb184 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx @@ -14,7 +14,7 @@ import AppInputsPanel from '@/app/components/plugins/plugin-detail-panel/app-sel import { AppPicker } from '@/app/components/plugins/plugin-detail-panel/app-selector/app-picker' import { AppTrigger } from '@/app/components/plugins/plugin-detail-panel/app-selector/app-trigger' import { consoleQuery } from '@/service/client' -import { useAppDetail } from '@/service/use-apps' +import { normalizeAppPagination, useAppDetail } from '@/service/use-apps' const PAGE_SIZE = 20 @@ -58,7 +58,7 @@ export function AppSelector({ fetchNextPage, hasNextPage, } = useInfiniteQuery({ - ...consoleQuery.apps.list.infiniteOptions({ + ...consoleQuery.apps.get.infiniteOptions({ input: pageParam => ({ query: { ...appListQuery, @@ -69,6 +69,10 @@ export function AppSelector({ initialPageParam: 1, placeholderData: keepPreviousData, }), + select: data => ({ + ...data, + pages: data.pages.map(normalizeAppPagination), + }), }) const displayedApps = useMemo(() => { diff --git a/web/app/components/snippet-list/components/__tests__/snippet-card.spec.tsx b/web/app/components/snippet-list/components/__tests__/snippet-card.spec.tsx index cd9b0d26380..6ca7a307433 100644 --- a/web/app/components/snippet-list/components/__tests__/snippet-card.spec.tsx +++ b/web/app/components/snippet-list/components/__tests__/snippet-card.spec.tsx @@ -228,7 +228,7 @@ describe('SnippetCard', () => { render( , diff --git a/web/app/components/workflow/block-selector/snippets/__tests__/snippet-list-item.spec.tsx b/web/app/components/workflow/block-selector/snippets/__tests__/snippet-list-item.spec.tsx index 6adbe547f84..aa31c131cb9 100644 --- a/web/app/components/workflow/block-selector/snippets/__tests__/snippet-list-item.spec.tsx +++ b/web/app/components/workflow/block-selector/snippets/__tests__/snippet-list-item.spec.tsx @@ -41,7 +41,7 @@ describe('SnippetListItem', () => { render( ({ vi.mock('@/service/client', () => ({ consoleQuery: { tags: { - list: { + get: { queryOptions: vi.fn(input => ({ queryKey: ['tags', input] })), }, }, @@ -24,8 +24,8 @@ describe('SnippetTagsFilter', () => { vi.clearAllMocks() mockUseQuery.mockReturnValue({ data: [ - { id: 'tag-1', name: 'Sales', type: 'snippet', binding_count: 1 }, - { id: 'tag-2', name: 'Support', type: 'snippet', binding_count: 2 }, + { id: 'tag-1', name: 'Sales', type: 'snippet', binding_count: '' }, + { id: 'tag-2', name: 'Support', type: 'snippet', binding_count: '' }, ], } as ReturnType) }) diff --git a/web/app/components/workflow/block-selector/snippets/snippet-tags-filter.tsx b/web/app/components/workflow/block-selector/snippets/snippet-tags-filter.tsx index 8ee15df1dce..9d5069f0b00 100644 --- a/web/app/components/workflow/block-selector/snippets/snippet-tags-filter.tsx +++ b/web/app/components/workflow/block-selector/snippets/snippet-tags-filter.tsx @@ -27,7 +27,7 @@ const SnippetTagsFilter = ({ const [open, setOpen] = useState(false) const [searchText, setSearchText] = useState('') - const { data: tagList = [] } = useQuery(consoleQuery.tags.list.queryOptions({ + const { data: tagList = [] } = useQuery(consoleQuery.tags.get.queryOptions({ input: { query: { type: 'snippet', diff --git a/web/app/education-apply/applied-education-content.tsx b/web/app/education-apply/applied-education-content.tsx index a7426bdd4e5..6f4b5e041d1 100644 --- a/web/app/education-apply/applied-education-content.tsx +++ b/web/app/education-apply/applied-education-content.tsx @@ -1,8 +1,9 @@ 'use client' +import type { TenantListItemResponse } from '@dify/contracts/api/console/workspaces/types.gen' import type { ReactNode } from 'react' import type { Plan as PlanType } from '@/app/components/billing/type' -import type { ICurrentWorkspace, IWorkspace } from '@/models/common' +import type { ICurrentWorkspace } from '@/models/common' import { Select, SelectTrigger, @@ -13,13 +14,19 @@ import { WorkplaceSelectorContent } from '@/app/components/header/account-dropdo import { PlanBadge } from '@/app/components/header/plan-badge' type AppliedEducationContentProps = { - workspaces: IWorkspace[] + workspaces: TenantListItemResponse[] currentWorkspace: ICurrentWorkspace plan: PlanType action: ReactNode onSwitchWorkspace: (tenantId: string) => void } +const workspacePlans = new Set(Object.values(Plan)) + +function isWorkspacePlan(plan: string | null | undefined): plan is Plan { + return !!plan && workspacePlans.has(plan) +} + const AppliedEducationContent = ({ workspaces, currentWorkspace, @@ -29,10 +36,10 @@ const AppliedEducationContent = ({ }: AppliedEducationContentProps) => { const { t } = useTranslation() const currentWorkspaceInList = workspaces.find(workspace => workspace.current) - const workspacePlan = Object.values(Plan).includes(currentWorkspaceInList?.plan as Plan) - ? currentWorkspaceInList?.plan as Plan - : Object.values(Plan).includes(plan as Plan) - ? plan as Plan + const workspacePlan = isWorkspacePlan(currentWorkspaceInList?.plan) + ? currentWorkspaceInList.plan + : isWorkspacePlan(plan) + ? plan : Plan.sandbox const workspaceName = currentWorkspaceInList?.name || currentWorkspace?.name const workspaceId = currentWorkspaceInList?.id || currentWorkspace?.id diff --git a/web/contract/console/agent-drive.ts b/web/contract/console/agent-drive.ts deleted file mode 100644 index b80af71ba24..00000000000 --- a/web/contract/console/agent-drive.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { type } from '@orpc/contract' -import { base } from '../base' - -type AgentDriveSkillItem = { - path: string - skill_md_key: string - archive_key?: string | null - name: string - description: string - size?: number | null - mime_type?: string | null - hash?: string | null - created_at?: number | null -} - -type AgentDriveSkillFile = { - available_in_drive: boolean - drive_key?: string | null - name: string - path: string - type: string -} - -type AgentDriveSkillMarkdown = { - binary: boolean - key: string - size?: number | null - text?: string | null - truncated: boolean -} - -type AgentDriveSkillInspect = AgentDriveSkillItem & { - file_tree?: Array> - files?: AgentDriveSkillFile[] - skill_md: AgentDriveSkillMarkdown - source: string - warnings?: string[] -} - -const agentDriveSkillsByAgentContract = base - .route({ - path: '/agent/{agent_id}/drive/skills', - method: 'GET', - }) - .input(type<{ - params: { - agent_id: string - } - }>()) - .output(type<{ items: AgentDriveSkillItem[] }>()) - -const agentDriveSkillInspectByAgentContract = base - .route({ - path: '/agent/{agent_id}/drive/skills/{skill_path}/inspect', - method: 'GET', - }) - .input(type<{ - params: { - agent_id: string - skill_path: string - } - }>()) - .output(type()) - -const agentDriveSkillsByAppContract = base - .route({ - path: '/apps/{app_id}/agent/drive/skills', - method: 'GET', - }) - .input(type<{ - params: { - app_id: string - } - query?: { - node_id?: string - } - }>()) - .output(type<{ items: AgentDriveSkillItem[] }>()) - -const agentDriveSkillInspectByAppContract = base - .route({ - path: '/apps/{app_id}/agent/drive/skills/{skill_path}/inspect', - method: 'GET', - }) - .input(type<{ - params: { - app_id: string - skill_path: string - } - query?: { - node_id?: string - } - }>()) - .output(type()) - -export const agentDriveContracts = { - byAgentId: { - drive: { - skills: { - get: agentDriveSkillsByAgentContract, - bySkillPath: { - inspect: { - get: agentDriveSkillInspectByAgentContract, - }, - }, - }, - }, - }, - byAppId: { - agent: { - drive: { - skills: { - get: agentDriveSkillsByAppContract, - bySkillPath: { - inspect: { - get: agentDriveSkillInspectByAppContract, - }, - }, - }, - }, - }, - }, -} diff --git a/web/contract/console/agent.ts b/web/contract/console/agent.ts deleted file mode 100644 index 2b2ee1ac938..00000000000 --- a/web/contract/console/agent.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { agent } from '@dify/contracts/api/console/agent/orpc.gen' -import { agentDriveContracts } from './agent-drive' - -export const agentRouterContract = { - ...agent, - byAgentId: { - ...agent.byAgentId, - drive: { - ...agent.byAgentId.drive, - ...agentDriveContracts.byAgentId.drive, - }, - }, -} diff --git a/web/contract/console/apps.ts b/web/contract/console/apps.ts deleted file mode 100644 index c52eb3883ab..00000000000 --- a/web/contract/console/apps.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { AppListResponse, WorkflowOnlineUsersResponse } from '@/models/app' -import type { CommonResponse } from '@/models/common' -import type { AppModeEnum } from '@/types/app' -import { apps } from '@dify/contracts/api/console/apps/orpc.gen' -import { type } from '@orpc/contract' -import { base } from '../base' -import { agentDriveContracts } from './agent-drive' - -export type AppListSortBy = 'last_modified' | 'recently_created' | 'earliest_created' -type AppListMode = AppModeEnum | 'agent' | 'channel' | 'all' - -export type AppListQuery = { - page?: number - limit?: number - name?: string - mode?: AppListMode - tag_ids?: string[] - creator_ids?: string[] - is_created_by_me?: boolean - sort_by?: AppListSortBy -} - -export const appListContract = base - .route({ - path: '/apps', - method: 'GET', - }) - .input(type<{ - query?: AppListQuery - }>()) - .output(type()) - -export const appDeleteContract = base - .route({ - path: '/apps/{appId}', - method: 'DELETE', - }) - .input(type<{ - params: { - appId: string - } - }>()) - .output(type()) - -export const appStarredListContract = base - .route({ - path: '/apps/starred', - method: 'GET', - }) - .input(type<{ - query?: AppListQuery - }>()) - .output(type()) - -export const appStarContract = base - .route({ - path: '/apps/{appId}/star', - method: 'POST', - }) - .input(type<{ - params: { - appId: string - } - }>()) - .output(type()) - -export const appUnstarContract = base - .route({ - path: '/apps/{appId}/star', - method: 'DELETE', - }) - .input(type<{ - params: { - appId: string - } - }>()) - .output(type()) - -export const workflowOnlineUsersContract = base - .route({ - path: '/apps/workflows/online-users', - method: 'POST', - }) - .input(type<{ - body: { - app_ids: string[] - } - }>()) - .output(type()) - -export const appsRouterContract = { - ...apps, - list: appListContract, - deleteApp: appDeleteContract, - starredList: appStarredListContract, - star: appStarContract, - unstar: appUnstarContract, - workflowOnlineUsers: workflowOnlineUsersContract, - byAppId: { - ...apps.byAppId, - agent: { - ...apps.byAppId.agent, - ...agentDriveContracts.byAppId.agent, - drive: { - ...apps.byAppId.agent.drive, - ...agentDriveContracts.byAppId.agent.drive, - }, - }, - }, -} diff --git a/web/contract/console/notification.ts b/web/contract/console/notification.ts deleted file mode 100644 index 8a167e0f9fd..00000000000 --- a/web/contract/console/notification.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { type } from '@orpc/contract' -import { base } from '../base' - -type ConsoleNotification = { - body: string - frequency: 'once' | 'always' - lang: string - notification_id: string - subtitle: string - title: string - title_pic_url?: string -} - -type ConsoleNotificationResponse = { - notifications: ConsoleNotification[] - should_show: boolean -} - -export const notificationContract = base - .route({ - path: '/notification', - method: 'GET', - }) - .output(type()) - -export const notificationDismissContract = base - .route({ - path: '/notification/dismiss', - method: 'POST', - }) - .input(type<{ - body: { - notification_id: string - } - }>()) - .output(type()) diff --git a/web/contract/console/tags.ts b/web/contract/console/tags.ts deleted file mode 100644 index 632729cbf9b..00000000000 --- a/web/contract/console/tags.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { tags } from '@dify/contracts/api/console/tags/orpc.gen' -import { type } from '@orpc/contract' -import { base } from '../base' - -export type TagType = 'knowledge' | 'app' | 'snippet' - -export type Tag = { - id: string - name: string - type: TagType - binding_count: number -} - -export const tagListContract = base - .route({ - path: '/tags', - method: 'GET', - }) - .input(type<{ - query: { - type: TagType - } - }>()) - .output(type()) - -export const tagCreateContract = base - .route({ - path: '/tags', - method: 'POST', - }) - .input(type<{ - body: { - name: string - type: TagType - } - }>()) - .output(type()) - -export const tagUpdateContract = base - .route({ - path: '/tags/{tagId}', - method: 'PATCH', - }) - .input(type<{ - params: { - tagId: string - } - body: { - name: string - } - }>()) - .output(type()) - -export const tagDeleteContract = base - .route({ - path: '/tags/{tagId}', - method: 'DELETE', - }) - .input(type<{ - params: { - tagId: string - } - }>()) - .output(type()) - -export const tagBindingCreateContract = base - .route({ - path: '/tag-bindings', - method: 'POST', - }) - .input(type<{ - body: { - tag_ids: string[] - target_id: string - type: TagType - } - }>()) - .output(type()) - -export const tagBindingRemoveContract = base - .route({ - path: '/tag-bindings/remove', - method: 'POST', - }) - .input(type<{ - body: { - tag_ids: string[] - target_id: string - type: TagType - } - }>()) - .output(type()) - -export const tagsRouterContract = { - ...tags, - list: tagListContract, - create: tagCreateContract, - update: tagUpdateContract, - delete: tagDeleteContract, - bind: tagBindingCreateContract, - unbind: tagBindingRemoveContract, -} diff --git a/web/contract/console/workspaces.ts b/web/contract/console/workspaces.ts deleted file mode 100644 index e81e0f22d14..00000000000 --- a/web/contract/console/workspaces.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { workspaces } from '@dify/contracts/api/console/workspaces/orpc.gen' -import { type } from '@orpc/contract' -import { base } from '../base' - -type WorkspaceListItem = { - id: string - name: string - plan: string - status: string - created_at: number - current: boolean -} - -export type GetWorkspacesResponse = { - workspaces: WorkspaceListItem[] -} - -export type SwitchWorkspaceRequest = { - tenant_id: string -} - -type SwitchedWorkspace = { - id: string - name: string | null - plan: string | null - status: string | null - created_at: number | null - role: string | null - in_trial: boolean | null - trial_end_reason: string | null - custom_config: Record | null - trial_credits: number | null - trial_credits_used: number | null - next_credit_reset_date: number | null -} - -export type SwitchWorkspaceResponse = { - result: 'success' | 'fail' - new_tenant: SwitchedWorkspace -} - -export const workspacesGetContract = base - .route({ - path: '/workspaces', - method: 'GET', - }) - .output(type()) - -export const workspaceSwitchContract = base - .route({ - path: '/workspaces/switch', - method: 'POST', - }) - .input(type<{ - body: SwitchWorkspaceRequest - }>()) - .output(type()) - -export const workspacesRouterContract = { - ...workspaces, - get: workspacesGetContract, - switch: { - post: workspaceSwitchContract, - }, -} diff --git a/web/contract/router.ts b/web/contract/router.ts index 012cdf096f2..bfffbaee73e 100644 --- a/web/contract/router.ts +++ b/web/contract/router.ts @@ -1,10 +1,12 @@ import { account } from '@dify/contracts/api/console/account/orpc.gen' import { activate } from '@dify/contracts/api/console/activate/orpc.gen' +import { agent } from '@dify/contracts/api/console/agent/orpc.gen' import { allWorkspaces } from '@dify/contracts/api/console/all-workspaces/orpc.gen' import { apiBasedExtension } from '@dify/contracts/api/console/api-based-extension/orpc.gen' import { apiKeyAuth } from '@dify/contracts/api/console/api-key-auth/orpc.gen' import { appDslVersion } from '@dify/contracts/api/console/app-dsl-version/orpc.gen' import { app } from '@dify/contracts/api/console/app/orpc.gen' +import { apps } from '@dify/contracts/api/console/apps/orpc.gen' import { auth } from '@dify/contracts/api/console/auth/orpc.gen' import { codeBasedExtension } from '@dify/contracts/api/console/code-based-extension/orpc.gen' import { compliance } from '@dify/contracts/api/console/compliance/orpc.gen' @@ -20,6 +22,7 @@ import { installedApps } from '@dify/contracts/api/console/installed-apps/orpc.g import { instructionGenerate } from '@dify/contracts/api/console/instruction-generate/orpc.gen' import { login } from '@dify/contracts/api/console/login/orpc.gen' import { logout } from '@dify/contracts/api/console/logout/orpc.gen' +import { notification } from '@dify/contracts/api/console/notification/orpc.gen' import { notion } from '@dify/contracts/api/console/notion/orpc.gen' import { oauth } from '@dify/contracts/api/console/oauth/orpc.gen' import { rag } from '@dify/contracts/api/console/rag/orpc.gen' @@ -32,37 +35,36 @@ import { ruleStructuredOutputGenerate } from '@dify/contracts/api/console/rule-s import { spec } from '@dify/contracts/api/console/spec/orpc.gen' import { systemFeatures } from '@dify/contracts/api/console/system-features/orpc.gen' import { tagBindings } from '@dify/contracts/api/console/tag-bindings/orpc.gen' +import { tags } from '@dify/contracts/api/console/tags/orpc.gen' import { test } from '@dify/contracts/api/console/test/orpc.gen' import { trialModels } from '@dify/contracts/api/console/trial-models/orpc.gen' import { website } from '@dify/contracts/api/console/website/orpc.gen' import { workflowGenerate } from '@dify/contracts/api/console/workflow-generate/orpc.gen' import { workflow } from '@dify/contracts/api/console/workflow/orpc.gen' +import { workspaces } from '@dify/contracts/api/console/workspaces/orpc.gen' import { contract as enterpriseContract } from '@dify/contracts/enterprise/orpc.gen' import { rbacAccessConfigContract } from './console/access-control' -import { agentRouterContract } from './console/agent' -import { appsRouterContract } from './console/apps' import { billingRouterContract } from './console/billing' import { exploreRouterContract } from './console/explore' import { filesRouterContract } from './console/files' import { modelProvidersRouterContract } from './console/model-providers' -import { notificationContract, notificationDismissContract } from './console/notification' import { pluginsRouterContract } from './console/plugins' import { snippetsRouterContract } from './console/snippets' -import { tagsRouterContract } from './console/tags' import { triggersRouterContract } from './console/trigger' import { trialAppsRouterContract } from './console/try-app' import { workflowDraftRouterContract } from './console/workflow' import { workflowCommentContracts } from './console/workflow-comment' -import { workspacesRouterContract } from './console/workspaces' const communityContract = { account, activate, + agent, allWorkspaces, apiBasedExtension, apiKeyAuth, app, appDslVersion, + apps, auth, codeBasedExtension, compliance, @@ -78,6 +80,7 @@ const communityContract = { instructionGenerate, login, logout, + notification, notion, oauth, rag, @@ -90,31 +93,27 @@ const communityContract = { spec, systemFeatures, tagBindings, + tags, test, trialModels, website, workflow, workflowGenerate, + workspaces, } export const consoleRouterContract = { enterprise: enterpriseContract, ...communityContract, - agent: agentRouterContract, - apps: appsRouterContract, billing: billingRouterContract, explore: exploreRouterContract, files: filesRouterContract, modelProviders: modelProvidersRouterContract, - notification: notificationContract, - notificationDismiss: notificationDismissContract, plugins: pluginsRouterContract, rbacAccessConfig: rbacAccessConfigContract, snippets: snippetsRouterContract, - tags: tagsRouterContract, triggers: triggersRouterContract, trialApps: trialAppsRouterContract, workflowComments: workflowCommentContracts, workflowDraft: workflowDraftRouterContract, - workspaces: workspacesRouterContract, } diff --git a/web/features/deployments/create-guide/state/__tests__/index.spec.ts b/web/features/deployments/create-guide/state/__tests__/index.spec.ts index 3a05d665e11..72b5f33a515 100644 --- a/web/features/deployments/create-guide/state/__tests__/index.spec.ts +++ b/web/features/deployments/create-guide/state/__tests__/index.spec.ts @@ -94,7 +94,7 @@ vi.mock('jotai-tanstack-query', () => ({ vi.mock('@/service/client', () => ({ consoleQuery: { apps: { - list: { + get: { infiniteOptions: (options: InfiniteQueryOptions) => ({ ...options, queryKey: ['sourceApps'], diff --git a/web/features/deployments/create-guide/state/source.ts b/web/features/deployments/create-guide/state/source.ts index b711a49a7e2..39bb834b8f1 100644 --- a/web/features/deployments/create-guide/state/source.ts +++ b/web/features/deployments/create-guide/state/source.ts @@ -7,6 +7,7 @@ import { atom } from 'jotai' import { atomWithInfiniteQuery, atomWithQuery } from 'jotai-tanstack-query' import { dslAppName, isWorkflowDsl } from '@/features/deployments/shared/domain/dsl' import { consoleQuery } from '@/service/client' +import { normalizeAppPagination } from '@/service/use-apps' import { AppModeEnum } from '@/types/app' import { dslFileAtom, dslFileReadVersionAtom, effectiveMethodAtom, selectedAppAtom, sourceSearchTextAtom } from './primitives' import { SOURCE_APPS_PAGE_SIZE } from './utils' @@ -71,7 +72,7 @@ export const importDslReadyAtom = atom((get) => { export const sourceAppsQueryAtom = atomWithInfiniteQuery((get) => { const sourceSearchText = get(sourceSearchTextAtom) - return consoleQuery.apps.list.infiniteOptions({ + return consoleQuery.apps.get.infiniteOptions({ input: pageParam => ({ query: { page: Number(pageParam), @@ -83,6 +84,10 @@ export const sourceAppsQueryAtom = atomWithInfiniteQuery((get) => { getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : undefined, initialPageParam: 1, placeholderData: keepPreviousData, + select: data => ({ + ...data, + pages: data.pages.map(normalizeAppPagination), + }), enabled: get(effectiveMethodAtom) === 'bindApp', }) }) diff --git a/web/features/deployments/create-release/state/__tests__/dsl-enabled.spec.ts b/web/features/deployments/create-release/state/__tests__/dsl-enabled.spec.ts index 8b7ab05e8c0..46ab9069664 100644 --- a/web/features/deployments/create-release/state/__tests__/dsl-enabled.spec.ts +++ b/web/features/deployments/create-release/state/__tests__/dsl-enabled.spec.ts @@ -103,7 +103,7 @@ vi.mock('jotai-tanstack-query', async (importOriginal) => { vi.mock('@/service/client', () => ({ consoleQuery: { apps: { - list: { + get: { infiniteOptions: (options: InfiniteQueryOptions) => ({ ...options, queryKey: ['sourceApps', options.input], diff --git a/web/features/deployments/create-release/state/index.ts b/web/features/deployments/create-release/state/index.ts index 0da75e39ebe..b47ea3243be 100644 --- a/web/features/deployments/create-release/state/index.ts +++ b/web/features/deployments/create-release/state/index.ts @@ -22,6 +22,7 @@ import { } from 'jotai-tanstack-query' import * as z from 'zod' import { consoleQuery } from '@/service/client' +import { normalizeAppPagination } from '@/service/use-apps' import { AppModeEnum } from '@/types/app' import { encodeDslContent, isWorkflowDsl } from '../../shared/domain/dsl' import { isDeploymentDslImportEnabled } from '../../shared/domain/feature-flags' @@ -245,7 +246,7 @@ export const createReleaseSourceAppSearchTextAtom = atom('') export const createReleaseSourceAppsQueryAtom = atomWithInfiniteQuery((get) => { const searchText = get(createReleaseSourceAppSearchTextAtom) - return consoleQuery.apps.list.infiniteOptions({ + return consoleQuery.apps.get.infiniteOptions({ input: pageParam => ({ query: { page: Number(pageParam), @@ -257,6 +258,10 @@ export const createReleaseSourceAppsQueryAtom = atomWithInfiniteQuery((get) => { getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : undefined, initialPageParam: 1, placeholderData: keepPreviousData, + select: data => ({ + ...data, + pages: data.pages.map(normalizeAppPagination), + }), enabled: Boolean( get(createReleaseDialogOpenAtom) && effectiveCreateReleaseSourceMode(get) === 'sourceApp' diff --git a/web/features/tag-management/__tests__/dataset-card-tags.spec.tsx b/web/features/tag-management/__tests__/dataset-card-tags.spec.tsx index 8b3cec572df..d4a6562deac 100644 --- a/web/features/tag-management/__tests__/dataset-card-tags.spec.tsx +++ b/web/features/tag-management/__tests__/dataset-card-tags.spec.tsx @@ -1,4 +1,4 @@ -import type { Tag } from '@/contract/console/tags' +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import { fireEvent, render, screen } from '@testing-library/react' import { describe, expect, it, vi } from 'vitest' import { DatasetCardTags } from '../components/dataset-card-tags' @@ -30,8 +30,8 @@ vi.mock('@/features/tag-management/components/tag-selector', () => ({ describe('DatasetCardTags', () => { const mockTags: Tag[] = [ - { id: 'tag-1', name: 'Tag 1', type: 'knowledge', binding_count: 0 }, - { id: 'tag-2', name: 'Tag 2', type: 'knowledge', binding_count: 0 }, + { id: 'tag-1', name: 'Tag 1', type: 'knowledge', binding_count: '' }, + { id: 'tag-2', name: 'Tag 2', type: 'knowledge', binding_count: '' }, ] const defaultProps = { @@ -163,7 +163,7 @@ describe('DatasetCardTags', () => { id: `tag-${i}`, name: `Tag ${i}`, type: 'knowledge', - binding_count: 0, + binding_count: '', })) render() expect(screen.getByText('20 tags')).toBeInTheDocument() diff --git a/web/features/tag-management/__tests__/tag-filter.spec.tsx b/web/features/tag-management/__tests__/tag-filter.spec.tsx index 8660f412b1e..5daadf1715b 100644 --- a/web/features/tag-management/__tests__/tag-filter.spec.tsx +++ b/web/features/tag-management/__tests__/tag-filter.spec.tsx @@ -1,4 +1,4 @@ -import type { Tag } from '@/contract/console/tags' +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { TagFilter } from '../components/tag-filter' @@ -22,10 +22,10 @@ vi.mock('@/context/app-context', () => ({ })) const mockTags: Tag[] = [ - { id: 'tag-1', name: 'Frontend', type: 'app', binding_count: 3 }, - { id: 'tag-2', name: 'Backend', type: 'app', binding_count: 5 }, - { id: 'tag-3', name: 'Database', type: 'knowledge', binding_count: 2 }, - { id: 'tag-4', name: 'API Design', type: 'app', binding_count: 1 }, + { id: 'tag-1', name: 'Frontend', type: 'app', binding_count: '' }, + { id: 'tag-2', name: 'Backend', type: 'app', binding_count: '' }, + { id: 'tag-3', name: 'Database', type: 'knowledge', binding_count: '' }, + { id: 'tag-4', name: 'API Design', type: 'app', binding_count: '' }, ] const defaultProps = { diff --git a/web/features/tag-management/__tests__/tag-item-editor.spec.tsx b/web/features/tag-management/__tests__/tag-item-editor.spec.tsx index 0114b677a68..cbb056b04ab 100644 --- a/web/features/tag-management/__tests__/tag-item-editor.spec.tsx +++ b/web/features/tag-management/__tests__/tag-item-editor.spec.tsx @@ -1,4 +1,4 @@ -import type { Tag } from '@/contract/console/tags' +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' @@ -37,15 +37,17 @@ vi.mock('@tanstack/react-query', () => ({ vi.mock('@/service/client', () => ({ consoleQuery: { tags: { - update: { - mutationOptions: () => ({ - mutationFn: ({ params, body }: { params: { tagId: string }, body: { name: string } }) => tagMocks.updateTag(params.tagId, body.name), - }), - }, - delete: { - mutationOptions: () => ({ - mutationFn: ({ params }: { params: { tagId: string } }) => tagMocks.deleteTag(params.tagId), - }), + byTagId: { + patch: { + mutationOptions: () => ({ + mutationFn: ({ params, body }: { params: { tag_id: string }, body: { name: string } }) => tagMocks.updateTag(params.tag_id, body.name), + }), + }, + delete: { + mutationOptions: () => ({ + mutationFn: ({ params }: { params: { tag_id: string } }) => tagMocks.deleteTag(params.tag_id), + }), + }, }, }, }, @@ -76,7 +78,7 @@ const baseTag: Tag = { id: 'tag-1', name: 'Frontend', type: 'app', - binding_count: 3, + binding_count: '3', } const i18n = { @@ -197,7 +199,7 @@ describe('TagItemEditor', () => { describe('Remove Flow', () => { it('should delete immediately when binding count is zero', async () => { const user = userEvent.setup() - const removableTag: Tag = { ...baseTag, binding_count: 0 } + const removableTag: Tag = { ...baseTag, binding_count: '' } render() const removeButton = screen.getByRole('button', { name: i18n.removeTag }) @@ -250,7 +252,7 @@ describe('TagItemEditor', () => { it('should notify error and keep tag when delete request fails', async () => { const user = userEvent.setup() vi.mocked(tagMocks.deleteTag).mockRejectedValueOnce(new Error('delete failed')) - const removableTag: Tag = { ...baseTag, binding_count: 0 } + const removableTag: Tag = { ...baseTag, binding_count: '' } render() const removeButton = screen.getByRole('button', { name: i18n.removeTag }) diff --git a/web/features/tag-management/__tests__/tag-management-modal.spec.tsx b/web/features/tag-management/__tests__/tag-management-modal.spec.tsx index ee67a561365..c09a6cb9822 100644 --- a/web/features/tag-management/__tests__/tag-management-modal.spec.tsx +++ b/web/features/tag-management/__tests__/tag-management-modal.spec.tsx @@ -1,4 +1,4 @@ -import type { Tag } from '@/contract/console/tags' +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' @@ -53,32 +53,34 @@ vi.mock('@/context/app-context', () => ({ vi.mock('@/service/client', () => ({ consoleQuery: { tags: { - list: { + get: { queryOptions: () => ({}), }, - create: { + post: { mutationOptions: () => ({ - mutationFn: ({ body }: { body: { name: string, type: 'app' | 'knowledge' } }) => createTag(body.name, body.type), + mutationFn: ({ body }: { body: { name: string, type: 'app' | 'knowledge' | 'snippet' } }) => createTag(body.name, body.type), }), }, - update: { - mutationOptions: () => ({ - mutationFn: () => Promise.resolve(undefined), - }), - }, - delete: { - mutationOptions: () => ({ - mutationFn: () => Promise.resolve(undefined), - }), + byTagId: { + patch: { + mutationOptions: () => ({ + mutationFn: () => Promise.resolve(undefined), + }), + }, + delete: { + mutationOptions: () => ({ + mutationFn: () => Promise.resolve(undefined), + }), + }, }, }, }, })) const mockTags: Tag[] = [ - { id: 'tag-1', name: 'Frontend', type: 'app', binding_count: 3 }, - { id: 'tag-2', name: 'Backend', type: 'app', binding_count: 5 }, - { id: 'tag-3', name: 'Database', type: 'knowledge', binding_count: 2 }, + { id: 'tag-1', name: 'Frontend', type: 'app', binding_count: '' }, + { id: 'tag-2', name: 'Backend', type: 'app', binding_count: '' }, + { id: 'tag-3', name: 'Database', type: 'knowledge', binding_count: '' }, ] const defaultProps = { @@ -100,7 +102,7 @@ describe('TagManagementModal', () => { vi.clearAllMocks() mockUseQueryData.current = mockTags mockWorkspacePermissionKeys.value = ['app.tag.manage', 'dataset.tag.manage', 'snippets.create_and_modify'] - vi.mocked(createTag).mockResolvedValue({ id: 'new-tag', name: 'NewTag', type: 'app', binding_count: 0 }) + vi.mocked(createTag).mockResolvedValue({ id: 'new-tag', name: 'NewTag', type: 'app', binding_count: '' }) }) describe('Rendering', () => { @@ -277,7 +279,7 @@ describe('TagManagementModal', () => { it('should handle tag creation with knowledge type', async () => { const user = userEvent.setup() - vi.mocked(createTag).mockResolvedValue({ id: 'new-k', name: 'KnowledgeTag', type: 'knowledge', binding_count: 0 }) + vi.mocked(createTag).mockResolvedValue({ id: 'new-k', name: 'KnowledgeTag', type: 'knowledge', binding_count: '' }) render() diff --git a/web/features/tag-management/__tests__/tag-search-content.spec.tsx b/web/features/tag-management/__tests__/tag-search-content.spec.tsx index 43679367605..a97f7283e9d 100644 --- a/web/features/tag-management/__tests__/tag-search-content.spec.tsx +++ b/web/features/tag-management/__tests__/tag-search-content.spec.tsx @@ -1,5 +1,5 @@ +import type { TagResponse as Tag, TagType } from '@dify/contracts/api/console/tags/types.gen' import type { TagComboboxItem } from '../components/tag-combobox-item' -import type { Tag, TagType } from '@/contract/console/tags' import { Combobox } from '@langgenius/dify-ui/combobox' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -30,12 +30,12 @@ const i18n = { } const appTags: Tag[] = [ - { id: 'tag-1', name: 'Frontend', type: 'app', binding_count: 3 }, - { id: 'tag-2', name: 'Backend', type: 'app', binding_count: 5 }, - { id: 'tag-3', name: 'API', type: 'app', binding_count: 1 }, + { id: 'tag-1', name: 'Frontend', type: 'app', binding_count: '' }, + { id: 'tag-2', name: 'Backend', type: 'app', binding_count: '' }, + { id: 'tag-3', name: 'API', type: 'app', binding_count: '' }, ] -const knowledgeTag: Tag = { id: 'tag-k1', name: 'KnowledgeDB', type: 'knowledge', binding_count: 2 } +const knowledgeTag: Tag = { id: 'tag-k1', name: 'KnowledgeDB', type: 'knowledge', binding_count: '' } type PanelHarnessProps = { type?: TagType @@ -68,7 +68,7 @@ const PanelHarness = ({ id: `__create_tag__:${inputValue}`, name: inputValue, type, - binding_count: 0, + binding_count: '', isCreateOption: true, }, ...tags] }, [inputValue, tagList, type]) @@ -251,7 +251,7 @@ describe('TagSearchContent', () => { , ) diff --git a/web/features/tag-management/__tests__/tag-selector.spec.tsx b/web/features/tag-management/__tests__/tag-selector.spec.tsx index a85f0348b56..754cf6a194c 100644 --- a/web/features/tag-management/__tests__/tag-selector.spec.tsx +++ b/web/features/tag-management/__tests__/tag-selector.spec.tsx @@ -1,5 +1,5 @@ +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import type { ComponentProps } from 'react' -import type { Tag } from '@/contract/console/tags' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { TagSelector } from '../components/tag-selector' @@ -54,12 +54,12 @@ vi.mock('@tanstack/react-query', () => ({ vi.mock('@/service/client', () => ({ consoleQuery: { tags: { - list: { + get: { queryOptions: () => ({}), }, - create: { + post: { mutationOptions: () => ({ - mutationFn: ({ body }: { body: { name: string, type: 'app' | 'knowledge' } }) => createTag(body.name, body.type), + mutationFn: ({ body }: { body: { name: string, type: 'app' | 'knowledge' | 'snippet' } }) => createTag(body.name, body.type), }), }, }, @@ -98,8 +98,8 @@ const i18n = { } const appTags: Tag[] = [ - { id: 'tag-1', name: 'Frontend', type: 'app', binding_count: 3 }, - { id: 'tag-2', name: 'Backend', type: 'app', binding_count: 5 }, + { id: 'tag-1', name: 'Frontend', type: 'app', binding_count: '' }, + { id: 'tag-2', name: 'Backend', type: 'app', binding_count: '' }, ] const defaultProps = { @@ -113,7 +113,7 @@ describe('TagSelector', () => { vi.clearAllMocks() mockWorkspacePermissionKeys.value = ['app.tag.manage', 'dataset.tag.manage', 'snippets.create_and_modify'] mockUseQueryData.current = appTags - vi.mocked(createTag).mockResolvedValue({ id: 'new-tag', name: 'NewTag', type: 'app', binding_count: 0 }) + vi.mocked(createTag).mockResolvedValue({ id: 'new-tag', name: 'NewTag', type: 'app', binding_count: '' }) vi.mocked(bindTag).mockResolvedValue(undefined) vi.mocked(unBindTag).mockResolvedValue(undefined) }) @@ -124,14 +124,14 @@ describe('TagSelector', () => { }) it('renders the no tag trigger when no current tag is visible and binding is unavailable', () => { - render() + render() expect(screen.queryByText('Orphan')).not.toBeInTheDocument() expect(screen.getByText(i18n.noTag)).toBeInTheDocument() expect(screen.queryByText(i18n.addTag)).not.toBeInTheDocument() }) it('renders the add tag trigger when no current tag is visible and binding is available', () => { - render() + render() expect(screen.queryByText('Orphan')).not.toBeInTheDocument() expect(screen.getByText(i18n.addTag)).toBeInTheDocument() }) @@ -315,13 +315,13 @@ describe('TagSelector', () => { it('opens snippet tag selector with snippets create-and-modify permission', async () => { const user = userEvent.setup() mockWorkspacePermissionKeys.value = ['snippets.create_and_modify'] - mockUseQueryData.current = [{ id: 'snippet-tag-1', name: 'Reusable', type: 'snippet', binding_count: 1 }] + mockUseQueryData.current = [{ id: 'snippet-tag-1', name: 'Reusable', type: 'snippet', binding_count: '' }] render( , ) diff --git a/web/features/tag-management/components/__tests__/app-card-tags.spec.tsx b/web/features/tag-management/components/__tests__/app-card-tags.spec.tsx index dd9ba09a88c..f6f65d82d7c 100644 --- a/web/features/tag-management/components/__tests__/app-card-tags.spec.tsx +++ b/web/features/tag-management/components/__tests__/app-card-tags.spec.tsx @@ -1,4 +1,4 @@ -import type { Tag } from '@/contract/console/tags' +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import { fireEvent, render, screen } from '@testing-library/react' import { describe, expect, it, vi } from 'vitest' import { AppCardTags } from '../app-card-tags' @@ -28,8 +28,8 @@ vi.mock('@/features/tag-management/components/tag-selector', () => ({ })) const tags: Tag[] = [ - { id: 'tag-1', name: 'Frontend', type: 'app', binding_count: 1 }, - { id: 'tag-2', name: 'Backend', type: 'app', binding_count: 2 }, + { id: 'tag-1', name: 'Frontend', type: 'app', binding_count: '' }, + { id: 'tag-2', name: 'Backend', type: 'app', binding_count: '' }, ] describe('AppCardTags', () => { diff --git a/web/features/tag-management/components/app-card-tags.tsx b/web/features/tag-management/components/app-card-tags.tsx index 67a3ca81d83..2187f4615d9 100644 --- a/web/features/tag-management/components/app-card-tags.tsx +++ b/web/features/tag-management/components/app-card-tags.tsx @@ -1,4 +1,4 @@ -import type { Tag } from '@/contract/console/tags' +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import { TagSelector } from '@/features/tag-management/components/tag-selector' type AppCardTagsProps = { diff --git a/web/features/tag-management/components/dataset-card-tags.tsx b/web/features/tag-management/components/dataset-card-tags.tsx index c33382b36bc..8075845a793 100644 --- a/web/features/tag-management/components/dataset-card-tags.tsx +++ b/web/features/tag-management/components/dataset-card-tags.tsx @@ -1,5 +1,5 @@ +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import type { MouseEvent } from 'react' -import type { Tag } from '@/contract/console/tags' import { cn } from '@langgenius/dify-ui/cn' import { TagSelector } from '@/features/tag-management/components/tag-selector' diff --git a/web/features/tag-management/components/tag-combobox-item.ts b/web/features/tag-management/components/tag-combobox-item.ts index 4a5846afeaf..ddd4e9677d0 100644 --- a/web/features/tag-management/components/tag-combobox-item.ts +++ b/web/features/tag-management/components/tag-combobox-item.ts @@ -1,10 +1,10 @@ -import type { Tag, TagType } from '@/contract/console/tags' +import type { TagResponse as Tag, TagType } from '@dify/contracts/api/console/tags/types.gen' type CreateTagOption = { id: string name: string type: TagType - binding_count: number + binding_count: string isCreateOption: true } diff --git a/web/features/tag-management/components/tag-filter.tsx b/web/features/tag-management/components/tag-filter.tsx index 5d908732acb..56c28776341 100644 --- a/web/features/tag-management/components/tag-filter.tsx +++ b/web/features/tag-management/components/tag-filter.tsx @@ -1,5 +1,5 @@ +import type { TagResponse as Tag, TagType } from '@dify/contracts/api/console/tags/types.gen' import type { ComboboxRootProps } from '@langgenius/dify-ui/combobox' -import type { Tag, TagType } from '@/contract/console/tags' import { cn } from '@langgenius/dify-ui/cn' import { Combobox, ComboboxContent, ComboboxTrigger } from '@langgenius/dify-ui/combobox' import { useQuery } from '@tanstack/react-query' @@ -31,7 +31,7 @@ export const TagFilter = ({ const [open, setOpen] = useState(false) const [inputValue, setInputValue] = useState('') - const { data: tagList = [] } = useQuery(consoleQuery.tags.list.queryOptions({ + const { data: tagList = [] } = useQuery(consoleQuery.tags.get.queryOptions({ input: { query: { type, diff --git a/web/features/tag-management/components/tag-item-editor.tsx b/web/features/tag-management/components/tag-item-editor.tsx index ac924494daa..45e9504fe39 100644 --- a/web/features/tag-management/components/tag-item-editor.tsx +++ b/web/features/tag-management/components/tag-item-editor.tsx @@ -1,4 +1,4 @@ -import type { Tag } from '@/contract/console/tags' +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import { AlertDialog, AlertDialogActions, @@ -27,8 +27,8 @@ type TagItemEditorProps = { } export const TagItemEditor = ({ tag, onTagsChange }: TagItemEditorProps) => { const { t } = useTranslation() - const updateTagMutation = useMutation(consoleQuery.tags.update.mutationOptions()) - const deleteTagMutation = useMutation(consoleQuery.tags.delete.mutationOptions()) + const updateTagMutation = useMutation(consoleQuery.tags.byTagId.patch.mutationOptions()) + const deleteTagMutation = useMutation(consoleQuery.tags.byTagId.delete.mutationOptions()) const [isEditing, setIsEditing] = useState(false) const editTag = (tagId: string, name: string) => { if (name === tag.name) { @@ -43,7 +43,7 @@ export const TagItemEditor = ({ tag, onTagsChange }: TagItemEditorProps) => { updateTagMutation.mutate({ params: { - tagId, + tag_id: tagId, }, body: { name, @@ -67,7 +67,7 @@ export const TagItemEditor = ({ tag, onTagsChange }: TagItemEditorProps) => { deleteTagMutation.mutate({ params: { - tagId, + tag_id: tagId, }, }, { onSuccess: () => { @@ -109,7 +109,7 @@ export const TagItemEditor = ({ tag, onTagsChange }: TagItemEditorProps) => { aria-label={`${t('operation.remove', { ns: 'common' })} ${tag.name}`} className="group/remove shrink-0 cursor-pointer rounded-md border-none bg-transparent p-1 hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden" onClick={() => { - if (tag.binding_count) + if (Number(tag.binding_count ?? 0) > 0) setShowRemoveModal(true) else handleRemove() diff --git a/web/features/tag-management/components/tag-management-modal.tsx b/web/features/tag-management/components/tag-management-modal.tsx index d21e26bca54..78d8dbb9d54 100644 --- a/web/features/tag-management/components/tag-management-modal.tsx +++ b/web/features/tag-management/components/tag-management-modal.tsx @@ -1,5 +1,5 @@ 'use client' -import type { TagType } from '@/contract/console/tags' +import type { TagType } from '@dify/contracts/api/console/tags/types.gen' import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQuery } from '@tanstack/react-query' @@ -21,7 +21,7 @@ export const TagManagementModal = ({ show, type, onClose, onTagsChange }: TagMan const { t } = useTranslation() const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) const canManageTags = hasPermission(workspacePermissionKeys, getTagManagePermissionKey(type)) - const { data: tagList = [] } = useQuery(consoleQuery.tags.list.queryOptions({ + const { data: tagList = [] } = useQuery(consoleQuery.tags.get.queryOptions({ input: { query: { type, @@ -29,7 +29,7 @@ export const TagManagementModal = ({ show, type, onClose, onTagsChange }: TagMan }, enabled: show && canManageTags, })) - const createTagMutation = useMutation(consoleQuery.tags.create.mutationOptions()) + const createTagMutation = useMutation(consoleQuery.tags.post.mutationOptions()) const [name, setName] = useState('') const createNewTag = () => { diff --git a/web/features/tag-management/components/tag-search-content.tsx b/web/features/tag-management/components/tag-search-content.tsx index 0e43fe50a8e..1ad87031ef1 100644 --- a/web/features/tag-management/components/tag-search-content.tsx +++ b/web/features/tag-management/components/tag-search-content.tsx @@ -1,5 +1,5 @@ +import type { TagType } from '@dify/contracts/api/console/tags/types.gen' import type { TagComboboxItem } from './tag-combobox-item' -import type { TagType } from '@/contract/console/tags' import { ComboboxEmpty, ComboboxInput, ComboboxInputGroup, ComboboxItem, ComboboxItemIndicator, ComboboxItemText, ComboboxList, ComboboxSeparator, useComboboxFilteredItems } from '@langgenius/dify-ui/combobox' import { Fragment } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/features/tag-management/components/tag-selector.tsx b/web/features/tag-management/components/tag-selector.tsx index 61c7146b2c5..1ee0063e6a0 100644 --- a/web/features/tag-management/components/tag-selector.tsx +++ b/web/features/tag-management/components/tag-selector.tsx @@ -1,7 +1,7 @@ +import type { TagResponse as Tag, TagType } from '@dify/contracts/api/console/tags/types.gen' import type { ComboboxRootProps } from '@langgenius/dify-ui/combobox' import type { ComponentProps } from 'react' import type { TagComboboxItem } from './tag-combobox-item' -import type { Tag, TagType } from '@/contract/console/tags' import { cn } from '@langgenius/dify-ui/cn' import { Combobox, ComboboxContent, ComboboxTrigger } from '@langgenius/dify-ui/combobox' import { toast } from '@langgenius/dify-ui/toast' @@ -78,8 +78,8 @@ export const TagSelector = ({ const { isPending: isCreatingTag, mutate: createTag, - } = useMutation(consoleQuery.tags.create.mutationOptions()) - const { data: tagList = [] } = useQuery(consoleQuery.tags.list.queryOptions({ + } = useMutation(consoleQuery.tags.post.mutationOptions()) + const { data: tagList = [] } = useQuery(consoleQuery.tags.get.queryOptions({ input: { query: { type, @@ -125,7 +125,7 @@ export const TagSelector = ({ id: `__create_tag__:${inputValue}`, name: inputValue, type, - binding_count: 0, + binding_count: '0', isCreateOption: true, }) } diff --git a/web/features/tag-management/hooks/__tests__/use-tag-mutations.spec.tsx b/web/features/tag-management/hooks/__tests__/use-tag-mutations.spec.tsx index fd73c38c126..32054cfc700 100644 --- a/web/features/tag-management/hooks/__tests__/use-tag-mutations.spec.tsx +++ b/web/features/tag-management/hooks/__tests__/use-tag-mutations.spec.tsx @@ -10,20 +10,22 @@ const { unbindTag, } = vi.hoisted(() => ({ bindTag: vi.fn(), - listKey: vi.fn((options: { type: 'query', input: { query: { type: string } } }) => ['console', 'tags', 'list', 'query', options.input.query.type]), + listKey: vi.fn((options: { type: 'query', input: { query: { type: string } } }) => ['console', 'tags', 'get', 'query', options.input.query.type]), unbindTag: vi.fn(), })) vi.mock('@/service/client', () => ({ consoleClient: { - tags: { - bind: bindTag, - unbind: unbindTag, + tagBindings: { + post: bindTag, + remove: { + post: unbindTag, + }, }, }, consoleQuery: { tags: { - list: { + get: { key: listKey, }, }, @@ -92,7 +94,7 @@ describe('useTagMutations', () => { }) await waitFor(() => { expect(invalidateQueries).toHaveBeenCalledWith({ - queryKey: ['console', 'tags', 'list', 'query', 'app'], + queryKey: ['console', 'tags', 'get', 'query', 'app'], }) }) expect(listKey).toHaveBeenCalledWith({ @@ -122,7 +124,7 @@ describe('useTagMutations', () => { expect(unbindTag).not.toHaveBeenCalled() await waitFor(() => { expect(invalidateQueries).toHaveBeenCalledWith({ - queryKey: ['console', 'tags', 'list', 'query', 'knowledge'], + queryKey: ['console', 'tags', 'get', 'query', 'knowledge'], }) }) expect(listKey).toHaveBeenCalledWith({ diff --git a/web/features/tag-management/hooks/use-tag-mutations.ts b/web/features/tag-management/hooks/use-tag-mutations.ts index 4b0dda9d826..22c7db123db 100644 --- a/web/features/tag-management/hooks/use-tag-mutations.ts +++ b/web/features/tag-management/hooks/use-tag-mutations.ts @@ -1,4 +1,4 @@ -import type { TagType } from '@/contract/console/tags' +import type { TagType } from '@dify/contracts/api/console/tags/types.gen' import { useMutation, useQueryClient } from '@tanstack/react-query' import { consoleClient, consoleQuery } from '@/service/client' @@ -20,7 +20,7 @@ export const useApplyTagBindingsMutation = () => { const operations: Promise[] = [] if (addTagIds.length) { - operations.push(consoleClient.tags.bind({ + operations.push(consoleClient.tagBindings.post({ body: { tag_ids: addTagIds, target_id: targetId, @@ -30,7 +30,7 @@ export const useApplyTagBindingsMutation = () => { } if (removeTagIds.length) { - operations.push(consoleClient.tags.unbind({ + operations.push(consoleClient.tagBindings.remove.post({ body: { tag_ids: removeTagIds, target_id: targetId, @@ -43,7 +43,7 @@ export const useApplyTagBindingsMutation = () => { }, onSettled: (_data, _error, variables) => { void queryClient.invalidateQueries({ - queryKey: consoleQuery.tags.list.key({ + queryKey: consoleQuery.tags.get.key({ type: 'query', input: { query: { diff --git a/web/features/tag-management/utils.ts b/web/features/tag-management/utils.ts index 08c7e22a8f9..f0d12d534a4 100644 --- a/web/features/tag-management/utils.ts +++ b/web/features/tag-management/utils.ts @@ -1,4 +1,4 @@ -import type { TagType } from '@/contract/console/tags' +import type { TagType } from '@dify/contracts/api/console/tags/types.gen' import type { PermissionKey } from '@/models/access-control' import { SnippetPermission } from '@/app/components/snippets/utils/permission' diff --git a/web/models/datasets.ts b/web/models/datasets.ts index b4614d7ac9d..70e1cd7f458 100644 --- a/web/models/datasets.ts +++ b/web/models/datasets.ts @@ -1,9 +1,9 @@ +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import type { DataSourceNotionPage, DataSourceProvider } from './common' import type { DatasourceType } from './pipeline' import type { IndexingType } from '@/app/components/datasets/create/step-two' import type { MetadataItemWithValue } from '@/app/components/datasets/metadata/types' import type { MetadataFilteringVariableType } from '@/app/components/workflow/nodes/knowledge-retrieval/types' -import type { Tag } from '@/contract/console/tags' import type { AppIconType, AppModeEnum, RetrievalConfig, TransferMethod } from '@/types/app' import type { SegmentImportStatus } from '@/types/dataset' import type { I18nKeysByPrefix } from '@/types/i18n' diff --git a/web/models/snippet.ts b/web/models/snippet.ts index cdf7c5b81c7..ccfefd7b306 100644 --- a/web/models/snippet.ts +++ b/web/models/snippet.ts @@ -1,6 +1,6 @@ +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import type { Viewport } from 'reactflow' import type { Edge, Node } from '@/app/components/workflow/types' -import type { Tag } from '@/contract/console/tags' import type { InputVar } from '@/models/pipeline' export type SnippetSection = 'orchestrate' diff --git a/web/service/client.spec.ts b/web/service/client.spec.ts index d1c9eab682a..2e1464787ec 100644 --- a/web/service/client.spec.ts +++ b/web/service/client.spec.ts @@ -1,7 +1,7 @@ import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen' +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import type { MutationFunctionContext } from '@tanstack/react-query' import type { consoleQuery as ConsoleQuery } from './client' -import type { Tag } from '@/contract/console/tags' import { QueryClient } from '@tanstack/react-query' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -32,7 +32,7 @@ const createTag = (overrides: Partial = {}): Tag => ({ id: 'tag-1', name: 'Frontend', type: 'app', - binding_count: 1, + binding_count: '1', ...overrides, }) @@ -588,14 +588,14 @@ describe('consoleQuery tag mutation defaults', () => { it('should add created tags to the matching list query cache', async () => { const consoleQuery = await loadConsoleQuery() const queryClient = new QueryClient() - const appListKey = consoleQuery.tags.list.queryKey({ + const appListKey = consoleQuery.tags.get.queryKey({ input: { query: { type: 'app', }, }, }) - const knowledgeListKey = consoleQuery.tags.list.queryKey({ + const knowledgeListKey = consoleQuery.tags.get.queryKey({ input: { query: { type: 'knowledge', @@ -613,13 +613,13 @@ describe('consoleQuery tag mutation defaults', () => { queryClient.setQueryData(appListKey, [existingAppTag]) queryClient.setQueryData(knowledgeListKey, [existingKnowledgeTag]) - const mutationOptions = consoleQuery.tags.create.mutationOptions() + const mutationOptions = consoleQuery.tags.post.mutationOptions() await mutationOptions.onSuccess?.( createdTag, { body: { name: createdTag.name, - type: createdTag.type, + type: 'app', }, }, undefined, @@ -633,14 +633,14 @@ describe('consoleQuery tag mutation defaults', () => { it('should update matching tags across cached list queries', async () => { const consoleQuery = await loadConsoleQuery() const queryClient = new QueryClient() - const appListKey = consoleQuery.tags.list.queryKey({ + const appListKey = consoleQuery.tags.get.queryKey({ input: { query: { type: 'app', }, }, }) - const knowledgeListKey = consoleQuery.tags.list.queryKey({ + const knowledgeListKey = consoleQuery.tags.get.queryKey({ input: { query: { type: 'knowledge', @@ -661,14 +661,14 @@ describe('consoleQuery tag mutation defaults', () => { const updatedTag = createTag({ ...targetTag, name: 'After', - binding_count: 5, + binding_count: '5', }) - const mutationOptions = consoleQuery.tags.update.mutationOptions() + const mutationOptions = consoleQuery.tags.byTagId.patch.mutationOptions() await mutationOptions.onSuccess?.( updatedTag, { params: { - tagId: targetTag.id, + tag_id: targetTag.id, }, body: { name: 'Ignored Client Name', @@ -688,14 +688,14 @@ describe('consoleQuery tag mutation defaults', () => { it('should remove deleted tags across cached list queries', async () => { const consoleQuery = await loadConsoleQuery() const queryClient = new QueryClient() - const appListKey = consoleQuery.tags.list.queryKey({ + const appListKey = consoleQuery.tags.get.queryKey({ input: { query: { type: 'app', }, }, }) - const knowledgeListKey = consoleQuery.tags.list.queryKey({ + const knowledgeListKey = consoleQuery.tags.get.queryKey({ input: { query: { type: 'knowledge', @@ -713,12 +713,12 @@ describe('consoleQuery tag mutation defaults', () => { queryClient.setQueryData(appListKey, [deletedTag, remainingTag]) queryClient.setQueryData(knowledgeListKey, [knowledgeTag]) - const mutationOptions = consoleQuery.tags.delete.mutationOptions() + const mutationOptions = consoleQuery.tags.byTagId.delete.mutationOptions() await mutationOptions.onSuccess?.( undefined, { params: { - tagId: deletedTag.id, + tag_id: deletedTag.id, }, }, undefined, diff --git a/web/service/client.ts b/web/service/client.ts index 896784683d5..7e31b817364 100644 --- a/web/service/client.ts +++ b/web/service/client.ts @@ -1,5 +1,6 @@ import type { AgentAppPagination } from '@dify/contracts/api/console/agent/types.gen' import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen' +import type { TagResponse as Tag, TagType } from '@dify/contracts/api/console/tags/types.gen' import type { GetReleaseResponse, ListReleasesResponse, @@ -10,7 +11,6 @@ import type { AnyContractRouter, ContractRouterClient } from '@orpc/contract' import type { JsonifiedClient } from '@orpc/openapi-client' import type { RouterUtils } from '@orpc/tanstack-query' import type { InfiniteData, QueryClient, QueryKey } from '@tanstack/react-query' -import type { Tag } from '@/contract/console/tags' import type { consoleRouterContract } from '@/contract/router' import { createORPCClient, onError } from '@orpc/client' import { OpenAPILink } from '@orpc/openapi-client/fetch' @@ -120,6 +120,10 @@ type AppDeployInvalidationOptions = { type ConsoleQueryUtils = RouterUtils>> +function isTagType(type: string | null | undefined): type is TagType { + return type === 'app' || type === 'knowledge' || type === 'snippet' +} + const defaultAppDeployInvalidationOptions = { appInstances: true, appInstanceSummaries: true, @@ -668,11 +672,14 @@ export const consoleQuery: RouterUtils = createTanstackQue }, }, tags: { - create: { + post: { mutationOptions: { onSuccess: (tag, _variables, _onMutateResult, context) => { + if (!isTagType(tag.type)) + return + context.client.setQueryData( - consoleQuery.tags.list.queryKey({ + consoleQuery.tags.get.queryKey({ input: { query: { type: tag.type, @@ -684,29 +691,31 @@ export const consoleQuery: RouterUtils = createTanstackQue }, }, }, - update: { - mutationOptions: { - onSuccess: (updatedTag, variables, _onMutateResult, context) => { - context.client.setQueriesData( - { - queryKey: consoleQuery.tags.list.key({ type: 'query' }), - }, - (oldTags: Tag[] | undefined) => oldTags?.map(tag => tag.id === variables.params.tagId - ? updatedTag - : tag), - ) + byTagId: { + patch: { + mutationOptions: { + onSuccess: (updatedTag, variables, _onMutateResult, context) => { + context.client.setQueriesData( + { + queryKey: consoleQuery.tags.get.key({ type: 'query' }), + }, + (oldTags: Tag[] | undefined) => oldTags?.map(tag => tag.id === variables.params.tag_id + ? updatedTag + : tag), + ) + }, }, }, - }, - delete: { - mutationOptions: { - onSuccess: (_data, variables, _onMutateResult, context) => { - context.client.setQueriesData( - { - queryKey: consoleQuery.tags.list.key({ type: 'query' }), - }, - (oldTags: Tag[] | undefined) => oldTags?.filter(tag => tag.id !== variables.params.tagId), - ) + delete: { + mutationOptions: { + onSuccess: (_data, variables, _onMutateResult, context) => { + context.client.setQueriesData( + { + queryKey: consoleQuery.tags.get.key({ type: 'query' }), + }, + (oldTags: Tag[] | undefined) => oldTags?.filter(tag => tag.id !== variables.params.tag_id), + ) + }, }, }, }, diff --git a/web/service/console-router-loader.spec.ts b/web/service/console-router-loader.spec.ts new file mode 100644 index 00000000000..faa5e8b430b --- /dev/null +++ b/web/service/console-router-loader.spec.ts @@ -0,0 +1,21 @@ +import { agent } from '@dify/contracts/api/console/agent/orpc.gen' +import { apps } from '@dify/contracts/api/console/apps/orpc.gen' +import { notification } from '@dify/contracts/api/console/notification/orpc.gen' +import { tags } from '@dify/contracts/api/console/tags/orpc.gen' +import { workspaces } from '@dify/contracts/api/console/workspaces/orpc.gen' +import { describe, expect, it } from 'vitest' +import { loadConsoleContractForSegment } from './console-router-loader' + +describe('loadConsoleContractForSegment', () => { + it.each([ + ['agent', 'agent', agent], + ['apps', 'apps', apps], + ['notification', 'notification', notification], + ['tags', 'tags', tags], + ['workspaces', 'workspaces', workspaces], + ] as const)('loads the generated %s contract when generated types are usable', async (segment, contractKey, generatedContract) => { + const contract = await loadConsoleContractForSegment(segment) + + expect((contract as Record)[contractKey]).toBe(generatedContract) + }) +}) diff --git a/web/service/console-router-loader.ts b/web/service/console-router-loader.ts index a84a73f1853..b56ed950e49 100644 --- a/web/service/console-router-loader.ts +++ b/web/service/console-router-loader.ts @@ -12,30 +12,22 @@ async function loadGeneratedConsoleContract(segment: string) { } const customConsoleContractLoaders: Record Promise> = { - agent: () => import('@/contract/console/agent').then(({ agentRouterContract }) => wrapConsoleContract('agent', agentRouterContract)), - apps: () => import('@/contract/console/apps').then(({ appsRouterContract }) => wrapConsoleContract('apps', appsRouterContract)), billing: () => import('@/contract/console/billing').then(({ billingRouterContract }) => wrapConsoleContract('billing', billingRouterContract)), enterprise: () => import('@dify/contracts/enterprise/orpc.gen').then(({ contract }) => wrapConsoleContract('enterprise', contract)), explore: () => import('@/contract/console/explore').then(({ exploreRouterContract }) => wrapConsoleContract('explore', exploreRouterContract)), files: () => import('@/contract/console/files').then(({ filesRouterContract }) => wrapConsoleContract('files', filesRouterContract)), modelProviders: () => import('@/contract/console/model-providers').then(({ modelProvidersRouterContract }) => wrapConsoleContract('modelProviders', modelProvidersRouterContract)), - notification: () => - import('@/contract/console/notification').then(({ notificationContract }) => wrapConsoleContract('notification', notificationContract)), - notificationDismiss: () => - import('@/contract/console/notification').then(({ notificationDismissContract }) => wrapConsoleContract('notificationDismiss', notificationDismissContract)), plugins: () => import('@/contract/console/plugins').then(({ pluginsRouterContract }) => wrapConsoleContract('plugins', pluginsRouterContract)), rbacAccessConfig: () => import('@/contract/console/access-control').then(({ rbacAccessConfigContract }) => wrapConsoleContract('rbacAccessConfig', rbacAccessConfigContract)), snippets: () => import('@/contract/console/snippets').then(({ snippetsRouterContract }) => wrapConsoleContract('snippets', snippetsRouterContract)), - tags: () => import('@/contract/console/tags').then(({ tagsRouterContract }) => wrapConsoleContract('tags', tagsRouterContract)), triggers: () => import('@/contract/console/trigger').then(({ triggersRouterContract }) => wrapConsoleContract('triggers', triggersRouterContract)), trialApps: () => import('@/contract/console/try-app').then(({ trialAppsRouterContract }) => wrapConsoleContract('trialApps', trialAppsRouterContract)), workflowComments: () => import('@/contract/console/workflow-comment').then(({ workflowCommentContracts }) => wrapConsoleContract('workflowComments', workflowCommentContracts)), workflowDraft: () => import('@/contract/console/workflow').then(({ workflowDraftRouterContract }) => wrapConsoleContract('workflowDraft', workflowDraftRouterContract)), - workspaces: () => import('@/contract/console/workspaces').then(({ workspacesRouterContract }) => wrapConsoleContract('workspaces', workspacesRouterContract)), } export async function loadConsoleContractForSegment(segment: string) { diff --git a/web/service/use-apps.ts b/web/service/use-apps.ts index 6a742d2a1fc..479844d6916 100644 --- a/web/service/use-apps.ts +++ b/web/service/use-apps.ts @@ -1,21 +1,25 @@ +import type { AppPagination, AppPartial } from '@dify/contracts/api/console/apps/types.gen' import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types' import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, + AppListResponse, AppStatisticsResponse, AppTokenCostsResponse, AppVoicesListResponse, WorkflowDailyConversationsResponse, } from '@/models/app' -import type { App } from '@/types/app' +import type { App, AppIconType } from '@/types/app' import { useMutation, useQuery, useQueryClient, } from '@tanstack/react-query' +import { AccessMode } from '@/models/access-control' import { consoleClient, consoleQuery } from '@/service/client' +import { AppModeEnum } from '@/types/app' import { get, post } from './base' const NAME_SPACE = 'apps' @@ -27,6 +31,80 @@ type DateRangeParams = { export const appDetailQueryKeyPrefix = [NAME_SPACE, 'detail'] const useAppFullListKey = [NAME_SPACE, 'full-list'] +const appIconTypes = new Set(['emoji', 'image', 'link']) +const appModes = new Set(Object.values(AppModeEnum)) +const accessModes = new Set(Object.values(AccessMode)) + +function isAppIconType(iconType: string | null | undefined): iconType is AppIconType { + return !!iconType && appIconTypes.has(iconType) +} + +function isAppMode(mode: string | null | undefined): mode is AppModeEnum { + return !!mode && appModes.has(mode) +} + +function isAccessMode(accessMode: string | null | undefined): accessMode is AccessMode { + return !!accessMode && accessModes.has(accessMode) +} + +function normalizeWorkflow(workflow: AppPartial['workflow']): App['workflow'] { + if (!workflow) + return undefined + + return { + id: workflow.id, + created_at: workflow.created_at ?? 0, + created_by: workflow.created_by ?? undefined, + updated_at: workflow.updated_at ?? 0, + updated_by: workflow.updated_by ?? undefined, + } +} + +function normalizeAppListItem(app: AppPartial): App { + const modelConfig = (app.model_config ?? {}) as App['model_config'] + + return { + id: app.id, + name: app.name, + description: app.description ?? '', + author_name: app.author_name ?? '', + icon_type: isAppIconType(app.icon_type) ? app.icon_type : null, + icon: app.icon ?? '', + icon_background: app.icon_background ?? null, + icon_url: app.icon_url, + use_icon_as_answer_icon: app.use_icon_as_answer_icon ?? false, + mode: isAppMode(app.mode) ? app.mode : AppModeEnum.CHAT, + enable_site: false, + enable_api: false, + api_rpm: 60, + api_rph: 3600, + is_demo: false, + is_starred: app.is_starred, + model_config: modelConfig, + app_model_config: modelConfig, + created_at: app.created_at ?? 0, + created_by: app.created_by ?? undefined, + maintainer: app.maintainer ?? undefined, + updated_at: app.updated_at ?? 0, + site: {} as App['site'], + api_base_url: '', + tags: app.tags ?? [], + workflow: normalizeWorkflow(app.workflow), + deleted_tools: [], + access_mode: isAccessMode(app.access_mode) ? app.access_mode : AccessMode.PUBLIC, + max_active_requests: app.max_active_requests, + has_draft_trigger: app.has_draft_trigger ?? undefined, + workflow_kind: null, + permission_keys: app.permission_keys, + } +} + +export function normalizeAppPagination(response: AppPagination): AppListResponse { + return { + ...response, + data: response.data.map(normalizeAppListItem), + } +} export const useGenerateRuleTemplate = (type: GeneratorType, disabled?: boolean) => { return useQuery({ @@ -54,7 +132,7 @@ export const useInvalidateAppList = () => { const queryClient = useQueryClient() return () => { queryClient.invalidateQueries({ - queryKey: consoleQuery.apps.list.key(), + queryKey: consoleQuery.apps.get.key(), }) } } @@ -63,16 +141,16 @@ export const useDeleteAppMutation = () => { const queryClient = useQueryClient() return useMutation({ - mutationKey: consoleQuery.apps.deleteApp.mutationKey(), + mutationKey: consoleQuery.apps.byAppId.delete.mutationKey(), mutationFn: (appId: string) => { - return consoleClient.apps.deleteApp({ - params: { appId }, + return consoleClient.apps.byAppId.delete({ + params: { app_id: appId }, }) }, onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ - queryKey: consoleQuery.apps.list.key(), + queryKey: consoleQuery.apps.get.key(), }), queryClient.invalidateQueries({ queryKey: useAppFullListKey, @@ -88,20 +166,20 @@ export const useToggleAppStarMutation = () => { return useMutation({ mutationFn: ({ appId, isStarred }: { appId: string, isStarred: boolean }) => { return isStarred - ? consoleClient.apps.unstar({ - params: { appId }, + ? consoleClient.apps.byAppId.star.delete({ + params: { app_id: appId }, }) - : consoleClient.apps.star({ - params: { appId }, + : consoleClient.apps.byAppId.star.post({ + params: { app_id: appId }, }) }, onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ - queryKey: consoleQuery.apps.list.key(), + queryKey: consoleQuery.apps.get.key(), }), queryClient.invalidateQueries({ - queryKey: consoleQuery.apps.starredList.key(), + queryKey: consoleQuery.apps.starred.get.key(), }), queryClient.invalidateQueries({ queryKey: useAppFullListKey, diff --git a/web/types/app.ts b/web/types/app.ts index 185c1699abe..1804acbfaca 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -1,6 +1,6 @@ +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' import type { CollectionType } from '@/app/components/tools/types' import type { UploadFileSetting } from '@/app/components/workflow/types' -import type { Tag } from '@/contract/console/tags' import type { LanguagesSupported } from '@/i18n-config/language' import type { AccessMode } from '@/models/access-control' import type { ExternalDataTool } from '@/models/common' diff --git a/web/types/snippet.ts b/web/types/snippet.ts index 7c7376721e6..27301bfff5a 100644 --- a/web/types/snippet.ts +++ b/web/types/snippet.ts @@ -1,4 +1,4 @@ -import type { Tag } from '@/contract/console/tags' +import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen' type SnippetType = 'node' | 'group'