mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
refactor(web): migrate console contracts to generated types (#38231)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
parent
089c3f4af0
commit
3ad06bebd9
@ -129,6 +129,7 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
})
|
||||
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
normalizeAppPagination: <T,>(response: T) => response,
|
||||
useDeleteAppMutation: () => ({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
|
||||
@ -104,6 +104,7 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
})
|
||||
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
normalizeAppPagination: <T,>(response: T) => response,
|
||||
useDeleteAppMutation: () => ({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -29,18 +29,22 @@ vi.mock(import('@/config'), async (importOriginal) => {
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
notification: {
|
||||
queryOptions: (options?: Record<string, unknown>) => ({
|
||||
queryKey: ['console', 'notification'],
|
||||
queryFn: (...args: unknown[]) => mockNotification(...args),
|
||||
...options,
|
||||
}),
|
||||
},
|
||||
notificationDismiss: {
|
||||
mutationOptions: (options?: Record<string, unknown>) => ({
|
||||
mutationKey: ['console', 'notificationDismiss'],
|
||||
mutationFn: (...args: unknown[]) => mockNotificationDismiss(...args),
|
||||
...options,
|
||||
}),
|
||||
get: {
|
||||
queryOptions: (options?: Record<string, unknown>) => ({
|
||||
queryKey: ['console', 'notification', 'get'],
|
||||
queryFn: (...args: unknown[]) => mockNotification(...args),
|
||||
...options,
|
||||
}),
|
||||
},
|
||||
dismiss: {
|
||||
post: {
|
||||
mutationOptions: (options?: Record<string, unknown>) => ({
|
||||
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'],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@ -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 (
|
||||
<InSiteMessage
|
||||
key={notification.notification_id}
|
||||
notificationId={notification.notification_id}
|
||||
key={notificationId}
|
||||
notificationId={notificationId}
|
||||
title={notification.title}
|
||||
subtitle={notification.subtitle}
|
||||
headerBgUrl={notification.title_pic_url}
|
||||
|
||||
@ -400,7 +400,7 @@ describe('AppCard', () => {
|
||||
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(<AppCard app={appWithTags} />)
|
||||
// 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(<AppCard app={firstApp} />)
|
||||
@ -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(<AppCard app={multiTagApp} />)
|
||||
@ -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(<AppCard app={multiTagApp} />)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<GetAppsData['query']>
|
||||
type AppListSortBy = NonNullable<AppListQuery['sort_by']>
|
||||
|
||||
type AppListHeaderFiltersProps = {
|
||||
category: AppListCategory
|
||||
tagIDs: string[]
|
||||
|
||||
@ -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<GetAppsData['query']>
|
||||
type AppListSortBy = NonNullable<AppListQuery['sort_by']>
|
||||
|
||||
type AppSortFilterProps = {
|
||||
value: AppListSortBy
|
||||
onChange: (value: AppListSortBy) => void
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<GetAppsData['query']>
|
||||
type AppListSortBy = NonNullable<AppListQuery['sort_by']>
|
||||
|
||||
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,
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@ -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(<DatasetCard dataset={dataset} />)
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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<string>(Object.values(Plan))
|
||||
|
||||
function isWorkspacePlan(plan: string | null | undefined): plan is Plan {
|
||||
return !!plan && workspacePlans.has(plan)
|
||||
}
|
||||
|
||||
const WorkplaceSelectorItem = memo(({
|
||||
workspace,
|
||||
}: WorkplaceSelectorItemProps) => (
|
||||
<SelectItem value={workspace.id} className="gap-2 py-1 pr-2 pl-3">
|
||||
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px]">
|
||||
<span className="h-6 bg-linear-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text align-middle leading-6 font-semibold text-shadow-shadow-1 uppercase opacity-90">
|
||||
{workspace.name[0]?.toLocaleUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<SelectItemText className="system-md-regular">{workspace.name}</SelectItemText>
|
||||
<PlanBadge plan={workspace.plan as Plan} />
|
||||
</SelectItem>
|
||||
))
|
||||
}: WorkplaceSelectorItemProps) => {
|
||||
const workspaceName = workspace.name || workspace.id
|
||||
const workspacePlan = isWorkspacePlan(workspace.plan) ? workspace.plan : Plan.sandbox
|
||||
|
||||
return (
|
||||
<SelectItem value={workspace.id} className="gap-2 py-1 pr-2 pl-3">
|
||||
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px]">
|
||||
<span className="h-6 bg-linear-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text align-middle leading-6 font-semibold text-shadow-shadow-1 uppercase opacity-90">
|
||||
{workspaceName[0]?.toLocaleUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<SelectItemText className="system-md-regular">{workspaceName}</SelectItemText>
|
||||
<PlanBadge plan={workspacePlan} />
|
||||
</SelectItem>
|
||||
)
|
||||
})
|
||||
WorkplaceSelectorItem.displayName = 'WorkplaceSelectorItem'
|
||||
|
||||
export const WorkplaceSelectorContent = memo(({
|
||||
|
||||
@ -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<string>(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({
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
<div className={workspaceSwitchListClassName}>
|
||||
{displayedWorkspaces.map(workspace => (
|
||||
<button
|
||||
type="button"
|
||||
key={workspace.id}
|
||||
aria-current={workspace.current ? 'true' : undefined}
|
||||
title={workspace.name}
|
||||
className={cn(
|
||||
'flex h-8 w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-1 text-left outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset',
|
||||
workspace.current && 'bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => {
|
||||
onSwitchWorkspace(workspace.id)
|
||||
}}
|
||||
>
|
||||
<WorkspaceMenuItemContent
|
||||
icon={<WorkspaceIcon name={workspace.name} className="h-5 w-5 rounded-md" />}
|
||||
label={workspace.name}
|
||||
trailing={workspace.current ? <span aria-hidden className="i-ri-check-line h-4 w-4 text-text-accent" /> : undefined}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
{displayedWorkspaces.map((workspace) => {
|
||||
const workspaceName = getWorkspaceName(workspace)
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={workspace.id}
|
||||
aria-current={workspace.current ? 'true' : undefined}
|
||||
title={workspaceName}
|
||||
className={cn(
|
||||
'flex h-8 w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-1 text-left outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset',
|
||||
workspace.current && 'bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => {
|
||||
onSwitchWorkspace(workspace.id)
|
||||
}}
|
||||
>
|
||||
<WorkspaceMenuItemContent
|
||||
icon={<WorkspaceIcon name={workspaceName} className="h-5 w-5 rounded-md" />}
|
||||
label={workspaceName}
|
||||
trailing={workspace.current ? <span aria-hidden className="i-ri-check-line h-4 w-4 text-text-accent" /> : undefined}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -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: <T,>(response: T) => response,
|
||||
useAppDetail: (appId: string) => ({
|
||||
data: apps.find(app => app.id === appId),
|
||||
}),
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -228,7 +228,7 @@ describe('SnippetCard', () => {
|
||||
|
||||
render(
|
||||
<SnippetCard
|
||||
snippet={createSnippet({ tags: [{ id: 'tag-1', name: 'Sales', type: 'snippet', binding_count: 1 }] })}
|
||||
snippet={createSnippet({ tags: [{ id: 'tag-1', name: 'Sales', type: 'snippet', binding_count: '' }] })}
|
||||
onOpenTagManagement={onOpenTagManagement}
|
||||
onTagsChange={onTagsChange}
|
||||
/>,
|
||||
|
||||
@ -41,7 +41,7 @@ describe('SnippetListItem', () => {
|
||||
render(
|
||||
<SnippetListItem
|
||||
snippet={createSnippet({
|
||||
tags: [{ id: 'tag-1', name: 'Search', type: 'snippet', binding_count: 1 }],
|
||||
tags: [{ id: 'tag-1', name: 'Search', type: 'snippet', binding_count: '' }],
|
||||
})}
|
||||
isHovered={false}
|
||||
onMouseEnter={vi.fn()}
|
||||
|
||||
@ -10,7 +10,7 @@ vi.mock('@tanstack/react-query', () => ({
|
||||
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<typeof useQuery>)
|
||||
})
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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<string>(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
|
||||
|
||||
@ -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<Record<string, unknown>>
|
||||
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<AgentDriveSkillInspect>())
|
||||
|
||||
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<AgentDriveSkillInspect>())
|
||||
|
||||
export const agentDriveContracts = {
|
||||
byAgentId: {
|
||||
drive: {
|
||||
skills: {
|
||||
get: agentDriveSkillsByAgentContract,
|
||||
bySkillPath: {
|
||||
inspect: {
|
||||
get: agentDriveSkillInspectByAgentContract,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
byAppId: {
|
||||
agent: {
|
||||
drive: {
|
||||
skills: {
|
||||
get: agentDriveSkillsByAppContract,
|
||||
bySkillPath: {
|
||||
inspect: {
|
||||
get: agentDriveSkillInspectByAppContract,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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<AppListResponse>())
|
||||
|
||||
export const appDeleteContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}',
|
||||
method: 'DELETE',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
appId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const appStarredListContract = base
|
||||
.route({
|
||||
path: '/apps/starred',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
query?: AppListQuery
|
||||
}>())
|
||||
.output(type<AppListResponse>())
|
||||
|
||||
export const appStarContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/star',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
appId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<CommonResponse>())
|
||||
|
||||
export const appUnstarContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/star',
|
||||
method: 'DELETE',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
appId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<CommonResponse>())
|
||||
|
||||
export const workflowOnlineUsersContract = base
|
||||
.route({
|
||||
path: '/apps/workflows/online-users',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
body: {
|
||||
app_ids: string[]
|
||||
}
|
||||
}>())
|
||||
.output(type<WorkflowOnlineUsersResponse>())
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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<ConsoleNotificationResponse>())
|
||||
|
||||
export const notificationDismissContract = base
|
||||
.route({
|
||||
path: '/notification/dismiss',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
body: {
|
||||
notification_id: string
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
@ -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<Tag[]>())
|
||||
|
||||
export const tagCreateContract = base
|
||||
.route({
|
||||
path: '/tags',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
body: {
|
||||
name: string
|
||||
type: TagType
|
||||
}
|
||||
}>())
|
||||
.output(type<Tag>())
|
||||
|
||||
export const tagUpdateContract = base
|
||||
.route({
|
||||
path: '/tags/{tagId}',
|
||||
method: 'PATCH',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
tagId: string
|
||||
}
|
||||
body: {
|
||||
name: string
|
||||
}
|
||||
}>())
|
||||
.output(type<Tag>())
|
||||
|
||||
export const tagDeleteContract = base
|
||||
.route({
|
||||
path: '/tags/{tagId}',
|
||||
method: 'DELETE',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
tagId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const tagBindingCreateContract = base
|
||||
.route({
|
||||
path: '/tag-bindings',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
body: {
|
||||
tag_ids: string[]
|
||||
target_id: string
|
||||
type: TagType
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const tagBindingRemoveContract = base
|
||||
.route({
|
||||
path: '/tag-bindings/remove',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
body: {
|
||||
tag_ids: string[]
|
||||
target_id: string
|
||||
type: TagType
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const tagsRouterContract = {
|
||||
...tags,
|
||||
list: tagListContract,
|
||||
create: tagCreateContract,
|
||||
update: tagUpdateContract,
|
||||
delete: tagDeleteContract,
|
||||
bind: tagBindingCreateContract,
|
||||
unbind: tagBindingRemoveContract,
|
||||
}
|
||||
@ -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<string, unknown> | 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<GetWorkspacesResponse>())
|
||||
|
||||
export const workspaceSwitchContract = base
|
||||
.route({
|
||||
path: '/workspaces/switch',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
body: SwitchWorkspaceRequest
|
||||
}>())
|
||||
.output(type<SwitchWorkspaceResponse>())
|
||||
|
||||
export const workspacesRouterContract = {
|
||||
...workspaces,
|
||||
get: workspacesGetContract,
|
||||
switch: {
|
||||
post: workspaceSwitchContract,
|
||||
},
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ vi.mock('jotai-tanstack-query', () => ({
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
apps: {
|
||||
list: {
|
||||
get: {
|
||||
infiniteOptions: (options: InfiniteQueryOptions) => ({
|
||||
...options,
|
||||
queryKey: ['sourceApps'],
|
||||
|
||||
@ -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',
|
||||
})
|
||||
})
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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(<DatasetCardTags {...defaultProps} tags={manyTags} />)
|
||||
expect(screen.getByText('20 tags')).toBeInTheDocument()
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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(<TagItemEditor tag={removableTag} />)
|
||||
|
||||
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(<TagItemEditor tag={removableTag} />)
|
||||
|
||||
const removeButton = screen.getByRole('button', { name: i18n.removeTag })
|
||||
|
||||
@ -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(<TagManagementModal {...defaultProps} type="knowledge" />)
|
||||
|
||||
|
||||
@ -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', () => {
|
||||
<PanelHarness
|
||||
type="snippet"
|
||||
value={[]}
|
||||
tagList={[{ id: 'snippet-tag-1', name: 'Reusable', type: 'snippet', binding_count: 1 }]}
|
||||
tagList={[{ id: 'snippet-tag-1', name: 'Reusable', type: 'snippet', binding_count: '' }]}
|
||||
/>,
|
||||
)
|
||||
|
||||
|
||||
@ -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(<TagSelector {...defaultProps} value={[{ id: 'orphan', name: 'Orphan', type: 'app', binding_count: 0 }]} />)
|
||||
render(<TagSelector {...defaultProps} value={[{ id: 'orphan', name: 'Orphan', type: 'app', binding_count: '' }]} />)
|
||||
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(<TagSelector {...defaultProps} value={[{ id: 'orphan', name: 'Orphan', type: 'app', binding_count: 0 }]} canBindOrUnbindTags />)
|
||||
render(<TagSelector {...defaultProps} value={[{ id: 'orphan', name: 'Orphan', type: 'app', binding_count: '' }]} canBindOrUnbindTags />)
|
||||
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(
|
||||
<TagSelector
|
||||
targetId="snippet-1"
|
||||
type="snippet"
|
||||
value={[{ id: 'snippet-tag-1', name: 'Reusable', type: 'snippet', binding_count: 1 }]}
|
||||
value={[{ id: 'snippet-tag-1', name: 'Reusable', type: 'snippet', binding_count: '' }]}
|
||||
/>,
|
||||
)
|
||||
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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<string>('')
|
||||
|
||||
const createNewTag = () => {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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<unknown>[] = []
|
||||
|
||||
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: {
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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> = {}): 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,
|
||||
|
||||
@ -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<JsonifiedClient<ContractRouterClient<typeof consoleRouterContract>>>
|
||||
|
||||
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<typeof consoleClient> = 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<typeof consoleClient> = 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),
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
21
web/service/console-router-loader.spec.ts
Normal file
21
web/service/console-router-loader.spec.ts
Normal file
@ -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<string, unknown>)[contractKey]).toBe(generatedContract)
|
||||
})
|
||||
})
|
||||
@ -12,30 +12,22 @@ async function loadGeneratedConsoleContract(segment: string) {
|
||||
}
|
||||
|
||||
const customConsoleContractLoaders: Record<string, () => Promise<AnyContractRouter>> = {
|
||||
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) {
|
||||
|
||||
@ -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<string>(['emoji', 'image', 'link'])
|
||||
const appModes = new Set<string>(Object.values(AppModeEnum))
|
||||
const accessModes = new Set<string>(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,
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user