mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 15:35:32 +08:00
feat: add agent version restore (#42567)
This commit is contained in:
@@ -5082,11 +5082,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/features/agent-v2/agent-detail/configure/use-agent-configure-build-draft.ts": {
|
||||
"@tanstack/query/prefer-query-options": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/features/agent-v2/agent-detail/logs/__tests__/page.spec.tsx": {
|
||||
"@tanstack/query/prefer-query-options": {
|
||||
"count": 1
|
||||
|
||||
@@ -535,10 +535,26 @@ vi.mock('../components/preview/header', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../components/preview/versions-panel', () => ({
|
||||
AgentPreviewVersionsPanel: (props: { onSelectVersion: (versionId: string) => void }) => (
|
||||
<button type="button" onClick={() => props.onSelectVersion('snapshot-2')}>
|
||||
select version
|
||||
</button>
|
||||
AgentPreviewVersionsPanel: (props: {
|
||||
onBeforeRestore?: () => Promise<void>
|
||||
onSelectVersion: (versionId: string | null) => void
|
||||
onVersionRestored?: () => Promise<void>
|
||||
}) => (
|
||||
<>
|
||||
<button type="button" onClick={() => props.onSelectVersion('snapshot-2')}>
|
||||
select version
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await props.onBeforeRestore?.()
|
||||
await props.onVersionRestored?.()
|
||||
props.onSelectVersion(null)
|
||||
}}
|
||||
>
|
||||
restore from version menu
|
||||
</button>
|
||||
</>
|
||||
),
|
||||
}))
|
||||
|
||||
@@ -2859,6 +2875,64 @@ describe('AgentConfigurePage', () => {
|
||||
expect(screen.queryByRole('region', { name: 'build-draft-bar' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should save pending draft edits before restoring from the version menu and rebase the composer', async () => {
|
||||
const user = userEvent.setup()
|
||||
const queryClient = createQueryClient()
|
||||
const draftSave = createDeferredPromise<{ agent_soul: object }>()
|
||||
mocks.saveComposerDraft.mockReturnValue(draftSave.promise)
|
||||
const refetchComposer = vi.fn(async () => {
|
||||
mocks.queryState.composer = {
|
||||
...mocks.queryState.composer,
|
||||
data: {
|
||||
agent_soul: {
|
||||
prompt: { system_prompt: 'restored prompt' },
|
||||
},
|
||||
},
|
||||
}
|
||||
return {}
|
||||
})
|
||||
mocks.queryState.composer = {
|
||||
data: {
|
||||
agent_soul: {
|
||||
prompt: { system_prompt: 'draft prompt' },
|
||||
},
|
||||
},
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
isSuccess: true,
|
||||
refetch: refetchComposer,
|
||||
}
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AgentConfigurePage agentId="agent-1" />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'edit prompt' }))
|
||||
await user.click(screen.getByRole('button', { name: 'open versions' }))
|
||||
await user.click(screen.getByRole('button', { name: 'restore from version menu' }))
|
||||
|
||||
expect(mocks.saveComposerDraft).toHaveBeenCalledTimes(1)
|
||||
expect(refetchComposer).not.toHaveBeenCalled()
|
||||
expect(screen.getByRole('region', { name: 'orchestrate-panel' })).toHaveTextContent(
|
||||
'prompt:edited draft prompt',
|
||||
)
|
||||
|
||||
draftSave.resolve({ agent_soul: {} })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('region', { name: 'orchestrate-panel' })).toHaveTextContent(
|
||||
'prompt:restored prompt',
|
||||
)
|
||||
})
|
||||
expect(refetchComposer).toHaveBeenCalledTimes(1)
|
||||
expect(screen.getByRole('region', { name: 'orchestrate-panel' })).toHaveTextContent(
|
||||
'readonly:no',
|
||||
)
|
||||
})
|
||||
|
||||
it('should rebase the composer from the restored version', async () => {
|
||||
const user = userEvent.setup()
|
||||
const queryClient = createQueryClient()
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { useAgentConfigureBuildDraftData } from '../use-agent-configure-build-draft'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
request: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/notifications', () => ({
|
||||
toast: { error: mocks.toastError },
|
||||
}))
|
||||
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleQuery: {
|
||||
agent: {
|
||||
byAgentId: {
|
||||
buildDraft: {
|
||||
get: {
|
||||
queryOptions: ({ context }: { context?: { silent?: boolean } }) => ({
|
||||
queryKey: ['agent', 'agent-1', 'build-draft'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await mocks.request()
|
||||
} catch (error) {
|
||||
if (!context?.silent) mocks.toastError('Request failed')
|
||||
throw error
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
function setup() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
})
|
||||
const setSoulSourceOverride = vi.fn()
|
||||
const wrapper = ({ children }: PropsWithChildren) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
const hook = renderHook(
|
||||
() =>
|
||||
useAgentConfigureBuildDraftData({
|
||||
agentId: 'agent-1',
|
||||
activeVersionId: 'version-1',
|
||||
isBuildMode: true,
|
||||
isViewingVersion: false,
|
||||
setSoulSourceOverride,
|
||||
soulSourceOverride: null,
|
||||
}),
|
||||
{ wrapper },
|
||||
)
|
||||
|
||||
return { ...hook, setSoulSourceOverride }
|
||||
}
|
||||
|
||||
describe('build draft refresh errors', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.request.mockReset()
|
||||
})
|
||||
|
||||
it('returns to the normal draft without a toast when a later refresh finds no build draft', async () => {
|
||||
mocks.request.mockResolvedValueOnce({ draft: { id: 'build-draft-1' } })
|
||||
const { result, setSoulSourceOverride } = setup()
|
||||
await waitFor(() => expect(result.current.isActive).toBe(true))
|
||||
|
||||
const notFound = new Response(null, { status: 404 })
|
||||
mocks.request.mockRejectedValueOnce(notFound)
|
||||
await act(async () => {
|
||||
const refreshed = await result.current.refetch()
|
||||
expect(refreshed.error).toBe(notFound)
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.soulSource).toBe('draft'))
|
||||
expect(setSoulSourceOverride).toHaveBeenCalledWith('draft')
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still notifies when refreshing the build draft fails unexpectedly', async () => {
|
||||
mocks.request.mockResolvedValueOnce({ draft: { id: 'build-draft-1' } })
|
||||
const { result, setSoulSourceOverride } = setup()
|
||||
await waitFor(() => expect(result.current.isActive).toBe(true))
|
||||
|
||||
const serverError = new Response(null, { status: 500 })
|
||||
mocks.request.mockRejectedValueOnce(serverError)
|
||||
await act(async () => {
|
||||
const refreshed = await result.current.refetch()
|
||||
expect(refreshed.error).toBe(serverError)
|
||||
})
|
||||
|
||||
expect(mocks.toastError).toHaveBeenCalledTimes(1)
|
||||
expect(setSoulSourceOverride).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -628,6 +628,22 @@ function AgentConfigurePageComposerContent({
|
||||
agentId={agentId}
|
||||
activeVersionId={activeVersionId}
|
||||
onSelectVersion={selectVersion}
|
||||
restoreDisabled={
|
||||
buildDraft.isActive ||
|
||||
buildDraftActionsDisabled ||
|
||||
isEnteringBuildMode ||
|
||||
isPublishing
|
||||
}
|
||||
onBeforeRestore={async () => {
|
||||
if (!isViewingVersion) {
|
||||
await waitForPendingPreviewDraftSave()
|
||||
await saveDraft()
|
||||
}
|
||||
}}
|
||||
onVersionRestored={async () => {
|
||||
await composerQuery.refetch()
|
||||
onComposerRebase()
|
||||
}}
|
||||
onClose={() => setShowPreviewVersions(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
+133
-17
@@ -14,11 +14,26 @@ import {
|
||||
agentComposerSavedDraftAtom,
|
||||
} from '@/features/agent-v2/agent-composer/store'
|
||||
import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
|
||||
import { AgentConfigurePublishBar } from '../publish-bar'
|
||||
|
||||
type PublishHandler = NonNullable<ComponentProps<typeof AgentConfigurePublishBar>['onPublish']>
|
||||
type PublishMock = Mock<PublishHandler>
|
||||
|
||||
const restoreAccess = vi.hoisted(() => ({
|
||||
canRestore: true,
|
||||
plan: 'professional',
|
||||
onUrlUpdate: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/features/agent-v2/permissions', () => ({
|
||||
useAgentPermissions: () => ({ canReleaseAndVersion: restoreAccess.canRestore }),
|
||||
}))
|
||||
vi.mock('@/features/system-features/state', async () => {
|
||||
const { atom } = await import('jotai')
|
||||
return { deploymentEditionAtom: atom('CLOUD') }
|
||||
})
|
||||
|
||||
const hotkeyRegistrations = vi.hoisted(
|
||||
() =>
|
||||
new Map<
|
||||
@@ -84,8 +99,24 @@ vi.mock('@/hooks/use-timestamp', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/features/system-features/client', () => ({
|
||||
systemFeaturesQueryOptions: () => ({
|
||||
queryKey: ['system-features'],
|
||||
queryFn: async () => ({ deployment_edition: 'CLOUD' }),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleQuery: {
|
||||
features: {
|
||||
get: {
|
||||
queryOptions: (options: Record<string, unknown>) => ({
|
||||
...options,
|
||||
queryKey: ['features'],
|
||||
queryFn: async () => ({ billing: { subscription: { plan: restoreAccess.plan } } }),
|
||||
}),
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
byAgentId: {
|
||||
get: {
|
||||
@@ -137,7 +168,10 @@ vi.mock('@/service/console', () => ({
|
||||
byVersionId: {
|
||||
restore: {
|
||||
post: {
|
||||
mutationOptions: () => ({ mutationFn: restoreVersionMutation }),
|
||||
mutationOptions: (options: Record<string, unknown>) => ({
|
||||
...options,
|
||||
mutationFn: restoreVersionMutation,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -239,6 +273,10 @@ function renderPublishBar({
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
queryClient.setQueryData<unknown>(consoleQuery.features.get.queryOptions({}).queryKey, {
|
||||
billing: { subscription: { plan: restoreAccess.plan } },
|
||||
})
|
||||
const { wrapper: NuqsWrapper } = createNuqsTestWrapper({ onUrlUpdate: restoreAccess.onUrlUpdate })
|
||||
const store = createStore()
|
||||
store.set(agentComposerPromptAtom, prompt)
|
||||
setupStore?.(store)
|
||||
@@ -270,20 +308,22 @@ function renderPublishBar({
|
||||
}
|
||||
|
||||
const renderPublishBarTree = (nextProps?: { isPublishing?: boolean }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<JotaiProvider store={store}>
|
||||
<AgentConfigurePublishBar
|
||||
agentId="agent-1"
|
||||
agentName="Iris"
|
||||
isPublishing={nextProps?.isPublishing ?? isPublishing}
|
||||
selectedVersionSnapshot={selectedVersionSnapshot}
|
||||
onPublish={onPublish}
|
||||
onExitVersions={onExitVersions}
|
||||
onOpenVersions={vi.fn()}
|
||||
onVersionRestored={onVersionRestored}
|
||||
/>
|
||||
</JotaiProvider>
|
||||
</QueryClientProvider>
|
||||
<NuqsWrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<JotaiProvider store={store}>
|
||||
<AgentConfigurePublishBar
|
||||
agentId="agent-1"
|
||||
agentName="Iris"
|
||||
isPublishing={nextProps?.isPublishing ?? isPublishing}
|
||||
selectedVersionSnapshot={selectedVersionSnapshot}
|
||||
onPublish={onPublish}
|
||||
onExitVersions={onExitVersions}
|
||||
onOpenVersions={vi.fn()}
|
||||
onVersionRestored={onVersionRestored}
|
||||
/>
|
||||
</JotaiProvider>
|
||||
</QueryClientProvider>
|
||||
</NuqsWrapper>
|
||||
)
|
||||
const view = render(renderPublishBarTree())
|
||||
|
||||
@@ -301,6 +341,8 @@ describe('AgentConfigurePublishBar', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
hotkeyRegistrations.clear()
|
||||
restoreAccess.canRestore = true
|
||||
restoreAccess.plan = 'professional'
|
||||
restoreVersionMutation.mockResolvedValue({
|
||||
active_config_snapshot_id: 'snapshot-2',
|
||||
result: 'success',
|
||||
@@ -393,8 +435,13 @@ describe('AgentConfigurePublishBar', () => {
|
||||
expect(screen.getByText('Stable version')).toBeInTheDocument()
|
||||
expect(screen.getByText('agentV2.agentDetail.versionHistory.viewOnly')).toBeInTheDocument()
|
||||
expect(screen.getByText('formatted:1710000000 · Alice')).toBeInTheDocument()
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: 'agentV2.agentDetail.versionHistory.restore' }),
|
||||
const user = userEvent.setup()
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2.agentDetail.versionHistory.restore/ }),
|
||||
)
|
||||
expect(restoreVersionMutation).not.toHaveBeenCalled()
|
||||
await user.click(
|
||||
within(await screen.findByRole('alertdialog')).getByRole('button', { name: /restore/i }),
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -413,6 +460,75 @@ describe('AgentConfigurePublishBar', () => {
|
||||
expect(toastMock.success).toHaveBeenCalledWith('common.api.actionSuccess')
|
||||
})
|
||||
|
||||
it('shows the agent upgrade dialog before opening pricing on the sandbox plan', async () => {
|
||||
restoreAccess.plan = 'sandbox'
|
||||
const user = userEvent.setup()
|
||||
const { onExitVersions, onVersionRestored } = renderPublishBar({
|
||||
selectedVersionSnapshot: activeConfigSnapshot,
|
||||
})
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2.agentDetail.versionHistory.restore/ }),
|
||||
)
|
||||
const dialog = await screen.findByRole('dialog', { name: 'billing.upgrade.agentRestore.title' })
|
||||
expect(dialog).toHaveAccessibleDescription('billing.upgrade.agentRestore.description')
|
||||
expect(restoreAccess.onUrlUpdate).not.toHaveBeenCalled()
|
||||
expect(restoreVersionMutation).not.toHaveBeenCalled()
|
||||
await user.click(
|
||||
within(dialog).getByRole('button', { name: 'billing.triggerLimitModal.upgrade' }),
|
||||
)
|
||||
expect(restoreAccess.onUrlUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ queryString: '?pricing=open' }),
|
||||
)
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
expect(restoreVersionMutation).not.toHaveBeenCalled()
|
||||
expect(onExitVersions).not.toHaveBeenCalled()
|
||||
expect(onVersionRestored).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('prevents duplicate restoration and cancellation while the request is pending', async () => {
|
||||
const deferred = createDeferredPromise()
|
||||
restoreVersionMutation.mockImplementationOnce(async () => {
|
||||
await deferred.promise
|
||||
return { active_config_snapshot_id: 'snapshot-2', result: 'success' }
|
||||
})
|
||||
const user = userEvent.setup()
|
||||
const { onExitVersions } = renderPublishBar({ selectedVersionSnapshot: activeConfigSnapshot })
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2.agentDetail.versionHistory.restore/ }),
|
||||
)
|
||||
const dialog = await screen.findByRole('alertdialog')
|
||||
const confirm = within(dialog).getByRole('button', { name: /restore/i })
|
||||
await user.click(confirm)
|
||||
await waitFor(() => expect(confirm).toHaveAttribute('aria-disabled', 'true'))
|
||||
expect(within(dialog).getByRole('button', { name: /cancel/i })).toBeDisabled()
|
||||
await user.click(confirm)
|
||||
expect(restoreVersionMutation).toHaveBeenCalledTimes(1)
|
||||
expect(onExitVersions).not.toHaveBeenCalled()
|
||||
await act(async () => {
|
||||
deferred.resolve()
|
||||
await deferred.promise
|
||||
})
|
||||
await waitFor(() => expect(onExitVersions).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
it('keeps the selected version open when restoration fails', async () => {
|
||||
restoreVersionMutation.mockRejectedValueOnce(new Error('Restore failed'))
|
||||
const user = userEvent.setup()
|
||||
const { onExitVersions, onVersionRestored } = renderPublishBar({
|
||||
selectedVersionSnapshot: activeConfigSnapshot,
|
||||
})
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2.agentDetail.versionHistory.restore/ }),
|
||||
)
|
||||
await user.click(
|
||||
within(await screen.findByRole('alertdialog')).getByRole('button', { name: /restore/i }),
|
||||
)
|
||||
await waitFor(() => expect(restoreVersionMutation).toHaveBeenCalledTimes(1))
|
||||
expect(onExitVersions).not.toHaveBeenCalled()
|
||||
expect(onVersionRestored).not.toHaveBeenCalled()
|
||||
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render saved time from the latest draft save timestamp', () => {
|
||||
renderPublishBar({ draftSavedAt: 1710000100000 })
|
||||
|
||||
|
||||
+35
-58
@@ -12,7 +12,7 @@ import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useId, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -21,6 +21,8 @@ import { isAgentComposerDirtyAtom } from '@/features/agent-v2/agent-composer/sto
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { AgentVersionRestoreDialogs } from '../../preview/versions-panel/restore-dialogs'
|
||||
import { useAgentVersionRestore } from '../../preview/versions-panel/use-agent-version-restore'
|
||||
import { AgentPublishImpactDetails } from './publish-impact-details'
|
||||
|
||||
const PUBLISH_AGENT_HOTKEY = 'Mod+Shift+P' satisfies Hotkey
|
||||
@@ -133,57 +135,15 @@ export function AgentConfigurePublishBar({
|
||||
enabled: publishIsAvailable && !isPublishing && !selectedVersionSnapshot,
|
||||
})
|
||||
useQuery(workflowReferencesQueryOptions)
|
||||
const restoreVersionMutation = useMutation(
|
||||
consoleQuery.agent.byAgentId.versions.byVersionId.restore.post.mutationOptions(),
|
||||
)
|
||||
const restore = useAgentVersionRestore({
|
||||
agentId,
|
||||
onRestored: async () => {
|
||||
await onVersionRestored?.()
|
||||
onExitVersions?.()
|
||||
},
|
||||
})
|
||||
const canPublish = publishIsAvailable && !isPublishing
|
||||
|
||||
const handleRestoreVersion = (versionId: string) => {
|
||||
if (restoreVersionMutation.isPending) return
|
||||
|
||||
restoreVersionMutation.mutate(
|
||||
{
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
version_id: versionId,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.agent.byAgentId.get.queryKey({
|
||||
input: {
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.agent.byAgentId.composer.get.queryKey({
|
||||
input: {
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.agent.byAgentId.versions.get.key(),
|
||||
}),
|
||||
])
|
||||
await onVersionRestored?.()
|
||||
onExitVersions?.()
|
||||
toast.success(tCommon(($) => $['api.actionSuccess']))
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(tCommon(($) => $['api.actionFailed']))
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handlePublish = async () => {
|
||||
if (!canPublish) return
|
||||
|
||||
@@ -237,12 +197,27 @@ export function AgentConfigurePublishBar({
|
||||
|
||||
if (selectedVersionSnapshot) {
|
||||
return (
|
||||
<AgentVersionRestoreBar
|
||||
version={selectedVersionSnapshot}
|
||||
isRestoring={restoreVersionMutation.isPending}
|
||||
onExitVersions={onExitVersions}
|
||||
onRestoreVersion={handleRestoreVersion}
|
||||
/>
|
||||
<>
|
||||
<AgentVersionRestoreBar
|
||||
version={selectedVersionSnapshot}
|
||||
isRestoring={restore.isPending}
|
||||
restoreDisabled={restore.disabled}
|
||||
onExitVersions={onExitVersions}
|
||||
onRestoreVersion={
|
||||
restore.canRestore ? () => restore.requestRestore(selectedVersionSnapshot) : undefined
|
||||
}
|
||||
/>
|
||||
<AgentVersionRestoreDialogs
|
||||
version={restore.version}
|
||||
isUpgradeOpen={restore.isUpgradeOpen}
|
||||
onUpgradeClose={restore.closeUpgrade}
|
||||
isConfirmOpen={restore.isConfirmOpen}
|
||||
onConfirmOpenChange={restore.onConfirmOpenChange}
|
||||
isPending={restore.isPending}
|
||||
disabled={restore.disabled}
|
||||
onConfirm={restore.confirmRestore}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -420,11 +395,13 @@ function PublishBarActions({
|
||||
function AgentVersionRestoreBar({
|
||||
version,
|
||||
isRestoring = false,
|
||||
restoreDisabled,
|
||||
onExitVersions,
|
||||
onRestoreVersion,
|
||||
}: {
|
||||
version: AgentConfigSnapshotSummaryResponse
|
||||
isRestoring?: boolean
|
||||
restoreDisabled?: boolean
|
||||
onExitVersions?: () => void
|
||||
onRestoreVersion?: (versionId: string) => void
|
||||
}) {
|
||||
@@ -461,9 +438,9 @@ function AgentVersionRestoreBar({
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
disabled={!onRestoreVersion}
|
||||
disabled={!onRestoreVersion || restoreDisabled}
|
||||
loading={isRestoring}
|
||||
className="h-8 rounded-lg px-3"
|
||||
className="shrink-0"
|
||||
onClick={() => onRestoreVersion?.(version.id)}
|
||||
>
|
||||
{t(($) => $['agentDetail.versionHistory.restore'])}
|
||||
|
||||
+171
-9
@@ -1,21 +1,26 @@
|
||||
import type { AgentConfigSnapshotSummaryResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import type { ReactElement } from 'react'
|
||||
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderWithAccountProfile as render } from '@/test/console/account-profile'
|
||||
import { renderWithAccountProfile as renderAccountProfile } from '@/test/console/account-profile'
|
||||
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
|
||||
import { AgentPreviewVersionsPanel } from '../versions-panel'
|
||||
|
||||
const exportState = vi.hoisted(() => ({
|
||||
canExport: true,
|
||||
canRestore: true,
|
||||
restoreVersion: vi.fn(async (_input: unknown) => ({ result: 'success' })),
|
||||
isExporting: false,
|
||||
edition: 'CLOUD',
|
||||
plan: 'professional' as string | undefined,
|
||||
exportAppDsl: vi.fn(),
|
||||
setPricing: vi.fn(),
|
||||
onUrlUpdate: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/features/agent-v2/permissions', () => ({
|
||||
useAgentPermissions: () => ({
|
||||
canImportExportDSL: exportState.canExport,
|
||||
canReleaseAndVersion: exportState.canRestore,
|
||||
agentQuery: { data: { app_id: 'app-1', name: 'My agent' } },
|
||||
}),
|
||||
}))
|
||||
@@ -32,10 +37,10 @@ vi.mock('jotai', async (importOriginal) => ({
|
||||
useAtomValue: () => exportState.edition,
|
||||
}))
|
||||
|
||||
vi.mock('nuqs', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('nuqs')>()),
|
||||
useQueryState: () => [null, exportState.setPricing],
|
||||
}))
|
||||
function render(ui: ReactElement, options?: Parameters<typeof renderAccountProfile>[1]) {
|
||||
const { wrapper: NuqsWrapper } = createNuqsTestWrapper({ onUrlUpdate: exportState.onUrlUpdate })
|
||||
return renderAccountProfile(<NuqsWrapper>{ui}</NuqsWrapper>, options)
|
||||
}
|
||||
|
||||
const versions: AgentConfigSnapshotSummaryResponse[] = [
|
||||
{
|
||||
@@ -79,6 +84,13 @@ vi.mock('@/hooks/use-timestamp', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/features/system-features/client', () => ({
|
||||
systemFeaturesQueryOptions: () => ({
|
||||
queryKey: ['system-features'],
|
||||
queryFn: async () => ({ deployment_edition: 'CLOUD' }),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleQuery: {
|
||||
features: { get: { queryOptions: () => ({ queryKey: ['features'] }) } },
|
||||
@@ -91,9 +103,22 @@ vi.mock('@/service/console', () => ({
|
||||
},
|
||||
agent: {
|
||||
byAgentId: {
|
||||
get: { queryKey: () => ['agent'] },
|
||||
composer: { get: { queryKey: () => ['composer'] } },
|
||||
versions: {
|
||||
byVersionId: {
|
||||
restore: {
|
||||
post: {
|
||||
mutationOptions: (options: Record<string, unknown>) => ({
|
||||
...options,
|
||||
mutationFn: exportState.restoreVersion,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
get: {
|
||||
queryOptions: () => ({ queryKey: ['agent-versions'] }),
|
||||
key: () => ['agent-versions'],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -105,6 +130,8 @@ describe('AgentPreviewVersionsPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
exportState.canExport = true
|
||||
exportState.canRestore = true
|
||||
exportState.restoreVersion.mockResolvedValue({ result: 'success' })
|
||||
exportState.isExporting = false
|
||||
exportState.edition = 'CLOUD'
|
||||
exportState.plan = 'professional'
|
||||
@@ -137,8 +164,9 @@ describe('AgentPreviewVersionsPanel', () => {
|
||||
expect(onSelectVersion).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('hides version actions without export permission', () => {
|
||||
it('hides version actions without export or restore permission', () => {
|
||||
exportState.canExport = false
|
||||
exportState.canRestore = false
|
||||
renderPanel()
|
||||
expect(screen.queryByRole('button', { name: /moreActions/ })).not.toBeInTheDocument()
|
||||
})
|
||||
@@ -149,7 +177,9 @@ describe('AgentPreviewVersionsPanel', () => {
|
||||
renderPanel()
|
||||
await user.click(screen.getByRole('button', { name: /moreActions.*Initial release/ }))
|
||||
await user.click(screen.getByRole('menuitem', { name: /export/i }))
|
||||
expect(exportState.setPricing).toHaveBeenCalledWith('open')
|
||||
expect(exportState.onUrlUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ queryString: '?pricing=open' }),
|
||||
)
|
||||
expect(exportState.exportAppDsl).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -181,6 +211,138 @@ describe('AgentPreviewVersionsPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Version restore', () => {
|
||||
const renderPanel = (onBeforeRestore?: () => void | Promise<void>) => {
|
||||
const onSelectVersion = vi.fn()
|
||||
const onVersionRestored = vi.fn().mockResolvedValue(undefined)
|
||||
render(
|
||||
<AgentPreviewVersionsPanel
|
||||
agentId="agent-1"
|
||||
activeVersionId="version-2"
|
||||
onSelectVersion={onSelectVersion}
|
||||
onVersionRestored={onVersionRestored}
|
||||
onBeforeRestore={onBeforeRestore}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
return { onSelectVersion, onVersionRestored }
|
||||
}
|
||||
|
||||
const openRestore = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||
await user.click(screen.getByRole('button', { name: /moreActions.*Initial release/ }))
|
||||
await user.click(screen.getByRole('menuitem', { name: /restore/i }))
|
||||
}
|
||||
|
||||
it('restores the version whose menu was opened only after confirmation, then returns to the refreshed draft', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onSelectVersion, onVersionRestored } = renderPanel()
|
||||
await openRestore(user)
|
||||
const dialog = await screen.findByRole('alertdialog')
|
||||
expect(exportState.restoreVersion).not.toHaveBeenCalled()
|
||||
expect(onSelectVersion).not.toHaveBeenCalled()
|
||||
await user.click(within(dialog).getByRole('button', { name: /restore/i }))
|
||||
await waitFor(() => expect(onSelectVersion).toHaveBeenCalledWith(null))
|
||||
expect(exportState.restoreVersion.mock.calls[0]?.[0]).toEqual({
|
||||
params: { agent_id: 'agent-1', version_id: 'version-0' },
|
||||
})
|
||||
expect(onVersionRestored.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
onSelectVersion.mock.invocationCallOrder[0]!,
|
||||
)
|
||||
})
|
||||
|
||||
it('does not restore when saving the existing draft fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onBeforeRestore = vi.fn().mockRejectedValue(new Error('Draft save failed'))
|
||||
const { onSelectVersion, onVersionRestored } = renderPanel(onBeforeRestore)
|
||||
await openRestore(user)
|
||||
await user.click(
|
||||
within(await screen.findByRole('alertdialog')).getByRole('button', { name: /restore/i }),
|
||||
)
|
||||
await waitFor(() => expect(onBeforeRestore).toHaveBeenCalledTimes(1))
|
||||
expect(exportState.restoreVersion).not.toHaveBeenCalled()
|
||||
expect(onSelectVersion).not.toHaveBeenCalled()
|
||||
expect(onVersionRestored).not.toHaveBeenCalled()
|
||||
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('cancels restoration without changing the selected version', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onSelectVersion } = renderPanel()
|
||||
await openRestore(user)
|
||||
await user.click(
|
||||
within(await screen.findByRole('alertdialog')).getByRole('button', { name: /cancel/i }),
|
||||
)
|
||||
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument())
|
||||
expect(exportState.restoreVersion).not.toHaveBeenCalled()
|
||||
expect(onSelectVersion).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('dismisses the agent upgrade dialog without restoring or opening pricing on the Cloud sandbox plan', async () => {
|
||||
exportState.plan = 'sandbox'
|
||||
const user = userEvent.setup()
|
||||
renderPanel()
|
||||
await openRestore(user)
|
||||
const dialog = await screen.findByRole('dialog', {
|
||||
name: 'billing.upgrade.agentRestore.title',
|
||||
})
|
||||
expect(dialog).toHaveAccessibleDescription('billing.upgrade.agentRestore.description')
|
||||
expect(exportState.onUrlUpdate).not.toHaveBeenCalled()
|
||||
expect(exportState.restoreVersion).not.toHaveBeenCalled()
|
||||
await user.click(
|
||||
within(dialog).getByRole('button', { name: 'billing.triggerLimitModal.dismiss' }),
|
||||
)
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
expect(exportState.onUrlUpdate).not.toHaveBeenCalled()
|
||||
expect(exportState.restoreVersion).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['COMMUNITY', 'ENTERPRISE'])(
|
||||
'allows restoration on %s without a Cloud plan',
|
||||
async (edition) => {
|
||||
exportState.edition = edition
|
||||
exportState.plan = undefined
|
||||
const user = userEvent.setup()
|
||||
renderPanel()
|
||||
await openRestore(user)
|
||||
await user.click(
|
||||
within(await screen.findByRole('alertdialog')).getByRole('button', { name: /restore/i }),
|
||||
)
|
||||
await waitFor(() => expect(exportState.restoreVersion).toHaveBeenCalledTimes(1))
|
||||
expect(exportState.onUrlUpdate).not.toHaveBeenCalled()
|
||||
},
|
||||
)
|
||||
|
||||
it('disables restoration until the Cloud plan is loaded', async () => {
|
||||
exportState.plan = undefined
|
||||
const user = userEvent.setup()
|
||||
renderPanel()
|
||||
await user.click(screen.getByRole('button', { name: /moreActions.*Initial release/ }))
|
||||
const restore = screen.getByRole('menuitem', { name: /restore/i })
|
||||
expect(restore).toHaveAttribute('aria-disabled', 'true')
|
||||
await user.click(restore)
|
||||
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
|
||||
expect(exportState.restoreVersion).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('offers restore independently of export permission', async () => {
|
||||
exportState.canExport = false
|
||||
const user = userEvent.setup()
|
||||
renderPanel()
|
||||
await user.click(screen.getByRole('button', { name: /moreActions.*Initial release/ }))
|
||||
expect(screen.getByRole('menuitem', { name: /restore/i })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('menuitem', { name: /export/i })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides restore without release permission while allowing export', async () => {
|
||||
exportState.canRestore = false
|
||||
const user = userEvent.setup()
|
||||
renderPanel()
|
||||
await user.click(screen.getByRole('button', { name: /moreActions.*Initial release/ }))
|
||||
expect(screen.queryByRole('menuitem', { name: /restore/i })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: /export/i })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Version selection', () => {
|
||||
it('should notify the selected version when a version row is clicked', () => {
|
||||
const handleSelectVersion = vi.fn()
|
||||
|
||||
+29
@@ -17,6 +17,8 @@ import { deploymentEditionAtom } from '@/features/system-features/state'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { CurrentDraftItem } from './current-draft-item'
|
||||
import { VersionFilter } from './filter'
|
||||
import { AgentVersionRestoreDialogs } from './restore-dialogs'
|
||||
import { useAgentVersionRestore } from './use-agent-version-restore'
|
||||
import { VersionFilterEmpty } from './version-filter-empty'
|
||||
import { VersionItem } from './version-item'
|
||||
|
||||
@@ -24,6 +26,9 @@ type AgentPreviewVersionsPanelProps = {
|
||||
agentId: string
|
||||
activeVersionId?: string | null
|
||||
onSelectVersion: (versionId: string | null) => void
|
||||
onBeforeRestore?: () => void | Promise<void>
|
||||
restoreDisabled?: boolean
|
||||
onVersionRestored?: () => void | Promise<void>
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
@@ -32,7 +37,19 @@ export function AgentPreviewVersionsPanel({
|
||||
activeVersionId,
|
||||
onSelectVersion,
|
||||
onClose,
|
||||
onVersionRestored,
|
||||
onBeforeRestore,
|
||||
restoreDisabled,
|
||||
}: AgentPreviewVersionsPanelProps) {
|
||||
const restore = useAgentVersionRestore({
|
||||
agentId,
|
||||
onBeforeRestore,
|
||||
disabled: restoreDisabled,
|
||||
onRestored: async () => {
|
||||
await onVersionRestored?.()
|
||||
onSelectVersion(null)
|
||||
},
|
||||
})
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const { t: tWorkflow } = useTranslation('workflow')
|
||||
@@ -140,6 +157,8 @@ export function AgentPreviewVersionsPanel({
|
||||
isLast={index === filteredVersions.length - 1}
|
||||
onSelect={onSelectVersion}
|
||||
onExport={canImportExportDSL ? handleExport : undefined}
|
||||
onRestore={restore.canRestore ? restore.requestRestore : undefined}
|
||||
restoreDisabled={restore.disabled}
|
||||
exportDisabled={exportDisabled}
|
||||
showUpgrade={shouldShowUpgrade}
|
||||
/>
|
||||
@@ -147,6 +166,16 @@ export function AgentPreviewVersionsPanel({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<AgentVersionRestoreDialogs
|
||||
version={restore.version}
|
||||
isUpgradeOpen={restore.isUpgradeOpen}
|
||||
onUpgradeClose={restore.closeUpgrade}
|
||||
isConfirmOpen={restore.isConfirmOpen}
|
||||
onConfirmOpenChange={restore.onConfirmOpenChange}
|
||||
isPending={restore.isPending}
|
||||
disabled={restore.disabled}
|
||||
onConfirm={restore.confirmRestore}
|
||||
/>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import type { AgentConfigSnapshotSummaryResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
AlertDialogCancelButton,
|
||||
AlertDialogConfirmButton,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
|
||||
|
||||
type AgentVersionRestoreDialogsProps = {
|
||||
version: AgentConfigSnapshotSummaryResponse | null
|
||||
isUpgradeOpen: boolean
|
||||
onUpgradeClose: () => void
|
||||
isConfirmOpen: boolean
|
||||
onConfirmOpenChange: (open: boolean) => void
|
||||
isPending: boolean
|
||||
disabled: boolean
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
export function AgentVersionRestoreDialogs({
|
||||
version,
|
||||
isUpgradeOpen,
|
||||
onUpgradeClose,
|
||||
isConfirmOpen,
|
||||
onConfirmOpenChange,
|
||||
isPending,
|
||||
disabled,
|
||||
onConfirm,
|
||||
}: AgentVersionRestoreDialogsProps) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const versionLabel = version
|
||||
? version.version_note ||
|
||||
t(($) => $['agentDetail.versionHistory.versionName'], { version: version.version })
|
||||
: ''
|
||||
|
||||
return (
|
||||
<>
|
||||
<PlanUpgradeModal
|
||||
show={isUpgradeOpen}
|
||||
onClose={onUpgradeClose}
|
||||
title={t(($) => $['upgrade.agentRestore.title'], { ns: 'billing' })}
|
||||
description={t(($) => $['upgrade.agentRestore.description'], { ns: 'billing' })}
|
||||
/>
|
||||
<AlertDialog open={isConfirmOpen} onOpenChange={onConfirmOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col gap-2 p-6 pb-4">
|
||||
<AlertDialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{`${t(($) => $['agentDetail.versionHistory.restore'])} ${versionLabel}`}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="system-md-regular text-text-secondary">
|
||||
{t(($) => $['versionHistory.restorationTip'], { ns: 'workflow' })}
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton variant="secondary" disabled={isPending}>
|
||||
{t(($) => $['operation.cancel'], { ns: 'common' })}
|
||||
</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton
|
||||
tone="default"
|
||||
disabled={disabled}
|
||||
loading={isPending}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{t(($) => $['agentDetail.versionHistory.restore'])}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import type { AgentConfigSnapshotSummaryResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { useAgentPermissions } from '@/features/agent-v2/permissions'
|
||||
import { deploymentEditionAtom } from '@/features/system-features/state'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
|
||||
export function useAgentVersionRestore({
|
||||
agentId,
|
||||
onBeforeRestore,
|
||||
disabled: externallyDisabled = false,
|
||||
onRestored,
|
||||
}: {
|
||||
agentId: string
|
||||
disabled?: boolean
|
||||
onBeforeRestore?: () => void | Promise<void>
|
||||
onRestored?: () => void | Promise<void>
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const queryClient = useQueryClient()
|
||||
const { canReleaseAndVersion: canRestore } = useAgentPermissions(agentId)
|
||||
const deploymentEdition = useAtomValue(deploymentEditionAtom)
|
||||
const { data: plan } = useQuery(
|
||||
consoleQuery.features.get.queryOptions({
|
||||
enabled: deploymentEdition === 'CLOUD',
|
||||
select: (data) => data.billing.subscription.plan,
|
||||
}),
|
||||
)
|
||||
const [isUpgradeOpen, setIsUpgradeOpen] = useState(false)
|
||||
const [version, setVersion] = useState<AgentConfigSnapshotSummaryResponse | null>(null)
|
||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false)
|
||||
const showUpgrade = deploymentEdition === 'CLOUD' && plan === 'sandbox'
|
||||
const disabled =
|
||||
externallyDisabled || !canRestore || (deploymentEdition === 'CLOUD' && plan === undefined)
|
||||
const mutation = useMutation(
|
||||
consoleQuery.agent.byAgentId.versions.byVersionId.restore.post.mutationOptions({
|
||||
onMutate: async () => {
|
||||
await onBeforeRestore?.()
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.agent.byAgentId.get.queryKey({
|
||||
input: { params: { agent_id: agentId } },
|
||||
}),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.agent.byAgentId.composer.get.queryKey({
|
||||
input: { params: { agent_id: agentId } },
|
||||
}),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.agent.byAgentId.versions.get.key(),
|
||||
}),
|
||||
])
|
||||
await onRestored?.()
|
||||
setIsConfirmOpen(false)
|
||||
toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' }))
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t(($) => $['api.actionFailed'], { ns: 'common' }))
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const requestRestore = (selectedVersion: AgentConfigSnapshotSummaryResponse) => {
|
||||
if (disabled || mutation.isPending) return
|
||||
if (showUpgrade) {
|
||||
setIsUpgradeOpen(true)
|
||||
return
|
||||
}
|
||||
setVersion(selectedVersion)
|
||||
setIsConfirmOpen(true)
|
||||
}
|
||||
|
||||
const confirmRestore = () => {
|
||||
if (!version || disabled || mutation.isPending) return
|
||||
if (showUpgrade) {
|
||||
setIsConfirmOpen(false)
|
||||
setIsUpgradeOpen(true)
|
||||
return
|
||||
}
|
||||
mutation.mutate({ params: { agent_id: agentId, version_id: version.id } })
|
||||
}
|
||||
|
||||
return {
|
||||
requestRestore,
|
||||
isPending: mutation.isPending,
|
||||
disabled: disabled || mutation.isPending,
|
||||
canRestore,
|
||||
showUpgrade,
|
||||
version,
|
||||
isUpgradeOpen,
|
||||
closeUpgrade: () => setIsUpgradeOpen(false),
|
||||
isConfirmOpen,
|
||||
onConfirmOpenChange: (open: boolean) => {
|
||||
if (!mutation.isPending) setIsConfirmOpen(open)
|
||||
},
|
||||
confirmRestore,
|
||||
}
|
||||
}
|
||||
+36
-15
@@ -39,6 +39,8 @@ export function VersionItem({
|
||||
isLast,
|
||||
onSelect,
|
||||
onExport,
|
||||
onRestore,
|
||||
restoreDisabled,
|
||||
exportDisabled,
|
||||
showUpgrade,
|
||||
}: {
|
||||
@@ -49,6 +51,8 @@ export function VersionItem({
|
||||
isLast: boolean
|
||||
onSelect: (versionId: string) => void
|
||||
onExport?: (versionId: string) => void
|
||||
onRestore?: (version: AgentConfigSnapshotSummaryResponse) => void
|
||||
restoreDisabled?: boolean
|
||||
exportDisabled?: boolean
|
||||
showUpgrade?: boolean
|
||||
}) {
|
||||
@@ -67,7 +71,7 @@ export function VersionItem({
|
||||
onClick={() => onSelect(version.id)}
|
||||
className={cn(
|
||||
'group relative flex w-full items-start gap-1 rounded-lg py-1 pl-2 text-left focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
|
||||
onExport ? 'pr-8' : 'pr-1.5',
|
||||
onExport || onRestore ? 'pr-8' : 'pr-1.5',
|
||||
isActive ? 'bg-state-accent-active' : 'hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
@@ -96,7 +100,7 @@ export function VersionItem({
|
||||
<VersionMetadata version={version} />
|
||||
</div>
|
||||
</button>
|
||||
{onExport && (
|
||||
{(onExport || onRestore) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
@@ -110,19 +114,36 @@ export function VersionItem({
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent placement="bottom-end" className="min-w-40">
|
||||
<DropdownMenuItem
|
||||
disabled={exportDisabled}
|
||||
onClick={() => onExport(version.id)}
|
||||
className="gap-2"
|
||||
>
|
||||
<span aria-hidden className="i-ri-file-download-line size-4 shrink-0" />
|
||||
<span className="flex-1">{t(($) => $.exportApp, { ns: 'app' })}</span>
|
||||
{showUpgrade && (
|
||||
<PremiumBadge size="s">
|
||||
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
{onRestore && (
|
||||
<DropdownMenuItem
|
||||
disabled={restoreDisabled}
|
||||
onClick={() => onRestore(version)}
|
||||
className="gap-2"
|
||||
>
|
||||
<span aria-hidden className="i-ri-history-line size-4 shrink-0" />
|
||||
<span className="flex-1">{tWorkflow(($) => $['common.restore'])}</span>
|
||||
{showUpgrade && (
|
||||
<PremiumBadge size="s">
|
||||
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{onExport && (
|
||||
<DropdownMenuItem
|
||||
disabled={exportDisabled}
|
||||
onClick={() => onExport(version.id)}
|
||||
className="gap-2"
|
||||
>
|
||||
<span aria-hidden className="i-ri-file-download-line size-4 shrink-0" />
|
||||
<span className="flex-1">{t(($) => $.exportApp, { ns: 'app' })}</span>
|
||||
{showUpgrade && (
|
||||
<PremiumBadge size="s">
|
||||
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
AgentFileNode,
|
||||
AgentSoulConfigFormState,
|
||||
} from '@/features/agent-v2/agent-composer/form-state'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import isEqual from 'fast-deep-equal'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -156,51 +156,42 @@ export function useAgentConfigureBuildDraftData({
|
||||
setSoulSourceOverride: (source: AgentConfigureSoulSource | null) => void
|
||||
soulSourceOverride: AgentConfigureSoulSource | null
|
||||
}) {
|
||||
const shouldSilenceBuildDraftCheckRef = useRef(true)
|
||||
const { t } = useTranslation('common')
|
||||
const buildDraftQueryInput = {
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
}
|
||||
const buildDraftQueryOptions = consoleQuery.agent.byAgentId.buildDraft.get.queryOptions({
|
||||
input: {
|
||||
params: buildDraftQueryInput.params,
|
||||
},
|
||||
context: {},
|
||||
})
|
||||
const silentBuildDraftQueryOptions = consoleQuery.agent.byAgentId.buildDraft.get.queryOptions({
|
||||
input: {
|
||||
params: buildDraftQueryInput.params,
|
||||
},
|
||||
context: {
|
||||
silent: true,
|
||||
},
|
||||
queryKey: buildDraftQueryOptions.queryKey,
|
||||
})
|
||||
const buildDraftQuery = useQuery({
|
||||
...buildDraftQueryOptions,
|
||||
enabled:
|
||||
isBuildMode &&
|
||||
!isViewingVersion &&
|
||||
soulSourceOverride !== 'draft' &&
|
||||
soulSourceOverride !== 'view-version',
|
||||
queryFn: async (context) => {
|
||||
try {
|
||||
const queryOptions = shouldSilenceBuildDraftCheckRef.current
|
||||
? silentBuildDraftQueryOptions
|
||||
: buildDraftQueryOptions
|
||||
|
||||
shouldSilenceBuildDraftCheckRef.current = false
|
||||
return await queryOptions.queryFn(context)
|
||||
} catch (error) {
|
||||
if (isNotFoundResponse(error)) setSoulSourceOverride('draft')
|
||||
throw error
|
||||
}
|
||||
},
|
||||
retry: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
const buildDraftQuery = useQuery(
|
||||
queryOptions({
|
||||
...buildDraftQueryOptions,
|
||||
enabled:
|
||||
isBuildMode &&
|
||||
!isViewingVersion &&
|
||||
soulSourceOverride !== 'draft' &&
|
||||
soulSourceOverride !== 'view-version',
|
||||
queryFn: async (context) => {
|
||||
try {
|
||||
return await buildDraftQueryOptions.queryFn(context)
|
||||
} catch (error) {
|
||||
if (isNotFoundResponse(error)) setSoulSourceOverride('draft')
|
||||
else toast.error(t(($) => $['api.actionFailed']))
|
||||
throw error
|
||||
}
|
||||
},
|
||||
retry: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
}),
|
||||
)
|
||||
const {
|
||||
data: buildDraftData,
|
||||
dataUpdatedAt: buildDraftDataUpdatedAt,
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "أحداث المشغل",
|
||||
"upgrade.addChunks.description": "لقد وصلت إلى الحد الأقصى لإضافة الأجزاء لهذا الخطة.",
|
||||
"upgrade.addChunks.title": "قم بالترقية لمواصلة إضافة المقاطع",
|
||||
"upgrade.agentRestore.description": "استعادة إصدارات تطبيق الوكيل غير متاحة في خطتك الحالية.",
|
||||
"upgrade.agentRestore.title": "قم بالترقية لاستعادة إصدارات تطبيق الوكيل",
|
||||
"upgrade.uploadMultipleFiles.description": "ارفع عدة مستندات دفعة واحدة وزِد الحد الأقصى لحجم كل ملف إلى 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "قم بالترقية لإتاحة رفع الملفات دفعة واحدة والملفات الأكبر حجمًا",
|
||||
"upgrade.uploadMultiplePages.description": "لقد وصلت إلى حد التحميل — يمكن اختيار ورفع مستند واحد فقط في كل مرة على الخطة الحالية الخاصة بك.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "Tətikləmə hadisələri",
|
||||
"upgrade.addChunks.description": "Bu plan üzrə fraqment əlavə etmə limitinə çatmısınız.",
|
||||
"upgrade.addChunks.title": "Fraqment əlavə etməyə davam etmək üçün planın yüksəldilməsi",
|
||||
"upgrade.agentRestore.description": "Cari planınızda agent tətbiqinin versiyalarının bərpası əlçatan deyil.",
|
||||
"upgrade.agentRestore.title": "Agent tətbiqinin versiyalarını bərpa etmək üçün planın yüksəldilməsi",
|
||||
"upgrade.uploadMultipleFiles.description": "Bir neçə sənədi eyni anda yüklə və hər faylın maksimum ölçüsünü 50 MB-a qədər artır.",
|
||||
"upgrade.uploadMultipleFiles.title": "Toplu yükləmə və daha böyük fayllar üçün planın yüksəldilməsi",
|
||||
"upgrade.uploadMultiplePages.description": "Yükləmə limitinə çatmısınız: cari planınızda eyni anda yalnız bir sənəd seçib yükləyə bilərsiniz.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "AUSLÖSEEREIGNISSE",
|
||||
"upgrade.addChunks.description": "Sie haben das Limit für das Hinzufügen von Abschnitten in diesem Tarif erreicht.",
|
||||
"upgrade.addChunks.title": "Upgraden, um weiterhin Abschnitte hinzuzufügen",
|
||||
"upgrade.agentRestore.description": "Die Wiederherstellung von Versionen der Agenten-App ist in Ihrem aktuellen Tarif nicht verfügbar.",
|
||||
"upgrade.agentRestore.title": "Upgraden, um Versionen der Agenten-App wiederherzustellen",
|
||||
"upgrade.uploadMultipleFiles.description": "Laden Sie mehrere Dokumente gleichzeitig hoch und erhöhen Sie die maximale Größe pro Datei auf 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "Upgrade durchführen, um Batch-Uploads und größere Dateien freizuschalten",
|
||||
"upgrade.uploadMultiplePages.description": "Sie haben das Upload-Limit erreicht – in Ihrem aktuellen Tarif kann jeweils nur ein Dokument ausgewählt und hochgeladen werden.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "TRIGGER EVENTS",
|
||||
"upgrade.addChunks.description": "You’ve reached the limit of adding chunks for this plan.",
|
||||
"upgrade.addChunks.title": "Upgrade to continue adding chunks",
|
||||
"upgrade.agentRestore.description": "Agent app version restoration is not available on your current plan.",
|
||||
"upgrade.agentRestore.title": "Upgrade to restore agent app versions",
|
||||
"upgrade.uploadMultipleFiles.description": "Upload multiple documents at once and increase the maximum size per file to 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "Upgrade to unlock batch uploads and larger files",
|
||||
"upgrade.uploadMultiplePages.description": "You’ve reached the upload limit — only one document can be selected and uploaded at a time on your current plan.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "EVENTOS DESENCADENANTES",
|
||||
"upgrade.addChunks.description": "Has alcanzado el límite de agregar fragmentos para este plan.",
|
||||
"upgrade.addChunks.title": "Actualiza para seguir agregando fragmentos",
|
||||
"upgrade.agentRestore.description": "La restauración de versiones de la aplicación de agente no está disponible en tu plan actual.",
|
||||
"upgrade.agentRestore.title": "Actualiza para restaurar versiones de la aplicación de agente",
|
||||
"upgrade.uploadMultipleFiles.description": "Sube varios documentos a la vez y aumenta el tamaño máximo por archivo a 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "Mejora tu plan para desbloquear las cargas por lotes y los archivos de mayor tamaño",
|
||||
"upgrade.uploadMultiplePages.description": "Has alcanzado el límite de carga: solo se puede seleccionar y subir un documento a la vez en tu plan actual.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "رویدادهای محرک",
|
||||
"upgrade.addChunks.description": "شما به حد اضافه کردن بخشها برای این طرح رسیدهاید.",
|
||||
"upgrade.addChunks.title": "برای ادامه افزودن بخشها ارتقا دهید",
|
||||
"upgrade.agentRestore.description": "بازیابی نسخههای برنامهٔ عامل در طرح فعلی شما در دسترس نیست.",
|
||||
"upgrade.agentRestore.title": "برای بازیابی نسخههای برنامهٔ عامل ارتقا دهید",
|
||||
"upgrade.uploadMultipleFiles.description": "چند سند را بهطور همزمان بارگذاری کنید و حداکثر اندازه هر فایل را به 50 MB افزایش دهید.",
|
||||
"upgrade.uploadMultipleFiles.title": "برای فعالکردن بارگذاری گروهی و فایلهای بزرگتر، ارتقا دهید",
|
||||
"upgrade.uploadMultiplePages.description": "شما به حد آپلود رسیدهاید — در طرح فعلی خود تنها میتوانید یک سند را در هر بار انتخاب و آپلود کنید.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "ÉVÉNEMENTS DÉCLENCHEURS",
|
||||
"upgrade.addChunks.description": "Vous avez atteint la limite d'ajout de morceaux pour ce plan.",
|
||||
"upgrade.addChunks.title": "Mettez à niveau pour continuer à ajouter des morceaux",
|
||||
"upgrade.agentRestore.description": "La restauration des versions de l’application d’agent n'est pas disponible avec votre plan actuel.",
|
||||
"upgrade.agentRestore.title": "Mettez à niveau pour restaurer les versions de l’application d’agent",
|
||||
"upgrade.uploadMultipleFiles.description": "Importez plusieurs documents à la fois et augmentez la taille maximale par fichier à 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "Passez à une offre supérieure pour débloquer les importations groupées et les fichiers plus volumineux",
|
||||
"upgrade.uploadMultiplePages.description": "Vous avez atteint la limite de téléchargement — un seul document peut être sélectionné et téléchargé à la fois avec votre abonnement actuel.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "ट्रिगर घटनाएँ",
|
||||
"upgrade.addChunks.description": "आप इस योजना के लिए टुकड़े जोड़ने की सीमा तक पहुँच चुके हैं।",
|
||||
"upgrade.addChunks.title": "अधिक चंक्स जोड़ने के लिए अपग्रेड करें",
|
||||
"upgrade.agentRestore.description": "आपकी वर्तमान योजना में एजेंट ऐप संस्करण पुनर्स्थापना उपलब्ध नहीं है।",
|
||||
"upgrade.agentRestore.title": "एजेंट ऐप संस्करणों को पुनर्स्थापित करने के लिए अपग्रेड करें",
|
||||
"upgrade.uploadMultipleFiles.description": "एक साथ कई दस्तावेज़ अपलोड करें और प्रति फ़ाइल अधिकतम साइज़ सीमा बढ़ाकर 50 MB करें।",
|
||||
"upgrade.uploadMultipleFiles.title": "बैच अपलोड और बड़ी फ़ाइलों की सुविधा अनलॉक करने के लिए अपग्रेड करें",
|
||||
"upgrade.uploadMultiplePages.description": "आपने अपलोड की सीमा तक पहुँच लिया है — आपके वर्तमान प्लान पर एक समय में केवल एक ही दस्तावेज़ चुना और अपलोड किया जा सकता है।",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "PERISTIWA PEMICU",
|
||||
"upgrade.addChunks.description": "Anda telah mencapai batas penambahan potongan untuk paket ini.",
|
||||
"upgrade.addChunks.title": "Tingkatkan untuk terus menambahkan potongan",
|
||||
"upgrade.agentRestore.description": "Pemulihan versi aplikasi agen tidak tersedia di paket Anda saat ini.",
|
||||
"upgrade.agentRestore.title": "Tingkatkan untuk memulihkan versi aplikasi agen",
|
||||
"upgrade.uploadMultipleFiles.description": "Upload beberapa dokumen sekaligus dan tingkatkan ukuran maksimum per file menjadi 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "Upgrade untuk membuka upload batch dan file berukuran lebih besar",
|
||||
"upgrade.uploadMultiplePages.description": "Anda telah mencapai batas unggah — hanya satu dokumen yang dapat dipilih dan diunggah sekaligus dengan paket Anda saat ini.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "EVENTI DI ATTIVAZIONE",
|
||||
"upgrade.addChunks.description": "Hai raggiunto il limite di aggiunta di blocchi per questo piano.",
|
||||
"upgrade.addChunks.title": "Aggiorna per continuare ad aggiungere blocchi",
|
||||
"upgrade.agentRestore.description": "Il ripristino delle versioni dell'app agente non è disponibile nel tuo piano attuale.",
|
||||
"upgrade.agentRestore.title": "Esegui l'upgrade per ripristinare le versioni dell'app agente",
|
||||
"upgrade.uploadMultipleFiles.description": "Carica più documenti contemporaneamente e aumenta la dimensione massima per file a 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "Passa a un piano superiore per sbloccare i caricamenti in batch e i file di dimensioni maggiori",
|
||||
"upgrade.uploadMultiplePages.description": "Hai raggiunto il limite di caricamento: sul tuo piano attuale può essere selezionato e caricato un solo documento alla volta.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "TRIGGER EVENTS",
|
||||
"upgrade.addChunks.description": "このプランでは、チャンク追加の上限に達しています。",
|
||||
"upgrade.addChunks.title": "アップグレードして、チャンクを引き続き追加できるようにしてください。",
|
||||
"upgrade.agentRestore.description": "現在のプランでは、エージェントアプリのバージョンの復元は利用できません。",
|
||||
"upgrade.agentRestore.title": "アップグレードしてエージェントアプリのバージョンを復元",
|
||||
"upgrade.uploadMultipleFiles.description": "複数のドキュメントを一度にアップロードでき、1ファイルあたりのサイズ上限も50 MBに引き上げられます。",
|
||||
"upgrade.uploadMultipleFiles.title": "アップグレードして一括アップロードと大容量ファイルを利用",
|
||||
"upgrade.uploadMultiplePages.description": "現在のプランではアップロード上限に達しています。1回の操作で選択・アップロードできるドキュメントは1つのみです。",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "트리거 이벤트",
|
||||
"upgrade.addChunks.description": "이 요금제에서는 더 이상 청크를 추가할 수 있는 한도에 도달했습니다.",
|
||||
"upgrade.addChunks.title": "계속해서 조각을 추가하려면 업그레이드하세요",
|
||||
"upgrade.agentRestore.description": "현재 플랜에서는 에이전트 앱 버전 복원을 사용할 수 없습니다.",
|
||||
"upgrade.agentRestore.title": "에이전트 앱 버전을 복원하려면 업그레이드하세요",
|
||||
"upgrade.uploadMultipleFiles.description": "여러 문서를 한 번에 업로드하고 파일당 최대 크기를 50MB로 늘리세요.",
|
||||
"upgrade.uploadMultipleFiles.title": "업그레이드하여 일괄 업로드와 대용량 파일을 이용하세요",
|
||||
"upgrade.uploadMultiplePages.description": "업로드 한도에 도달했습니다 — 현재 요금제에서는 한 번에 한 개의 문서만 선택하고 업로드할 수 있습니다.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "ເຫດການຕົວເລີ່ມຕົ້ນ",
|
||||
"upgrade.addChunks.description": "ທ່ານໄດ້ໃຊ້ງານຮອດຂີດຈຳກັດໃນການເພີ່ມ Chunk ສຳລັບແພັກເກດນີ້ແລ້ວ.",
|
||||
"upgrade.addChunks.title": "ອັບເກຣດເພື່ອເພີ່ມ Chunk ຕໍ່",
|
||||
"upgrade.agentRestore.description": "ການກູ້ຄືນເວີຊັນແອັບ Agent ບໍ່ສາມາດໃຊ້ງານໄດ້ໃນແພັກເກດປັດຈຸບັນຂອງທ່ານ.",
|
||||
"upgrade.agentRestore.title": "ອັບເກຣດເພື່ອກູ້ຄືນເວີຊັນແອັບ Agent",
|
||||
"upgrade.uploadMultipleFiles.description": "ອັບໂຫລດເອກະສານຫຼາຍອັນພ້ອມກັນເພື່ອປະຢັດເວລາ ແລະ ເພີ່ມປະສິດທິພາບ.",
|
||||
"upgrade.uploadMultipleFiles.title": "ອັບເກຣດເພື່ອປົດລັອກການອັບໂຫລດເອກະສານແບບເປັນກຸ່ມ",
|
||||
"upgrade.uploadMultiplePages.description": "ທ່ານຮອດຂີດຈຳກັດການອັບໂຫລດແລ້ວ — ສາມາດເລືອກ ແລະ ອັບໂຫລດເອກະສານໄດ້ເທື່ອລະອັນໃນແພັກເກດປັດຈຸບັນ.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "TRIGGER EVENTS",
|
||||
"upgrade.addChunks.description": "You’ve reached the limit of adding chunks for this plan.",
|
||||
"upgrade.addChunks.title": "Upgrade to continue adding chunks",
|
||||
"upgrade.agentRestore.description": "Het herstellen van versies van de agent-app is niet beschikbaar in je huidige abonnement.",
|
||||
"upgrade.agentRestore.title": "Upgrade om versies van de agent-app te herstellen",
|
||||
"upgrade.uploadMultipleFiles.description": "Upload meerdere documenten tegelijk en verhoog de maximale bestandsgrootte naar 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "Upgrade je abonnement om batchuploads en grotere bestanden te ontgrendelen",
|
||||
"upgrade.uploadMultiplePages.description": "You’ve reached the upload limit — only one document can be selected and uploaded at a time on your current plan.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "WYDARZENIA WYZWALAJĄCE",
|
||||
"upgrade.addChunks.description": "Osiągnąłeś limit dodawania fragmentów w tym planie.",
|
||||
"upgrade.addChunks.title": "Uaktualnij, aby kontynuować dodawanie fragmentów",
|
||||
"upgrade.agentRestore.description": "Przywracanie wersji aplikacji agenta nie jest dostępne w obecnym planie.",
|
||||
"upgrade.agentRestore.title": "Ulepsz plan, aby przywracać wersje aplikacji agenta",
|
||||
"upgrade.uploadMultipleFiles.description": "Przesyłaj wiele dokumentów jednocześnie i zwiększ maksymalny rozmiar pojedynczego pliku do 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "Przejdź na wyższy plan, aby odblokować przesyłanie zbiorcze i większe pliki",
|
||||
"upgrade.uploadMultiplePages.description": "Osiągnąłeś limit przesyłania — w ramach obecnego planu można wybrać i przesłać tylko jeden dokument naraz.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "EVENTOS DE GATILHO",
|
||||
"upgrade.addChunks.description": "Você atingiu o limite de adição de blocos para este plano.",
|
||||
"upgrade.addChunks.title": "Faça upgrade para continuar adicionando blocos",
|
||||
"upgrade.agentRestore.description": "A restauração de versões do aplicativo de agente não está disponível no seu plano atual.",
|
||||
"upgrade.agentRestore.title": "Faça upgrade para restaurar versões do aplicativo de agente",
|
||||
"upgrade.uploadMultipleFiles.description": "Envie vários documentos de uma só vez e aumente o tamanho máximo por arquivo para 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "Faça upgrade para liberar uploads em lote e arquivos maiores",
|
||||
"upgrade.uploadMultiplePages.description": "Você atingiu o limite de upload — apenas um documento pode ser selecionado e enviado por vez no seu plano atual.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "EVENIMENTE DECLANȘATOARE",
|
||||
"upgrade.addChunks.description": "Ai atins limita de adăugare a segmentelor pentru acest plan.",
|
||||
"upgrade.addChunks.title": "Actualizează pentru a continua să adaugi segmente",
|
||||
"upgrade.agentRestore.description": "Restaurarea versiunilor aplicației agent nu este disponibilă în planul tău actual.",
|
||||
"upgrade.agentRestore.title": "Fă upgrade pentru a restaura versiunile aplicației agent",
|
||||
"upgrade.uploadMultipleFiles.description": "Încarcă mai multe documente simultan și mărește dimensiunea maximă pentru fiecare fișier la 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "Treci la un plan superior pentru a debloca încărcările în lot și fișierele mai mari",
|
||||
"upgrade.uploadMultiplePages.description": "Ați atins limita de încărcare — poate fi selectat și încărcat doar un singur document odată în planul dvs. actual.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "СОБЫТИЯ-ИНИЦИАТОРЫ",
|
||||
"upgrade.addChunks.description": "Вы достигли предела добавления чанков по этому тарифному плану.",
|
||||
"upgrade.addChunks.title": "Обновите версию, чтобы продолжить добавление блоков",
|
||||
"upgrade.agentRestore.description": "Восстановление версий приложения агента недоступно в вашем текущем плане.",
|
||||
"upgrade.agentRestore.title": "Обновите план, чтобы восстанавливать версии приложения агента",
|
||||
"upgrade.uploadMultipleFiles.description": "Загружайте несколько документов одновременно и увеличьте максимальный размер одного файла до 50 МБ.",
|
||||
"upgrade.uploadMultipleFiles.title": "Перейдите на более высокий тариф, чтобы разблокировать пакетную загрузку и файлы большего размера",
|
||||
"upgrade.uploadMultiplePages.description": "Вы достигли лимита загрузки — на вашем текущем тарифном плане можно выбрать и загрузить только один документ за раз.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "SPROŽITVENI DOGODKI",
|
||||
"upgrade.addChunks.description": "Dosegli ste omejitev dodajanja delov za ta načrt.",
|
||||
"upgrade.addChunks.title": "Nadgradite, da nadaljujete z dodajanjem delov",
|
||||
"upgrade.agentRestore.description": "Obnovitev različic aplikacije agenta v vašem trenutnem paketu ni na voljo.",
|
||||
"upgrade.agentRestore.title": "Nadgradite za obnovitev različic aplikacije agenta",
|
||||
"upgrade.uploadMultipleFiles.description": "Naložite več dokumentov hkrati in povečajte največjo velikost posamezne datoteke na 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "Nadgradite paket, da odklenete paketno nalaganje in večje datoteke",
|
||||
"upgrade.uploadMultiplePages.description": "Dosegli ste omejitev nalaganja — na vašem trenutnem načrtu je mogoče izbrati in naložiti le en dokument naenkrat.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "เหตุการณ์ทริกเกอร์",
|
||||
"upgrade.addChunks.description": "คุณได้ถึงขีดจำกัดในการเพิ่มชิ้นส่วนสำหรับแผนนี้แล้ว",
|
||||
"upgrade.addChunks.title": "อัปเกรดเพื่อเพิ่มชิ้นส่วนต่อ",
|
||||
"upgrade.agentRestore.description": "การกู้คืนเวอร์ชันแอปเอเจนต์ไม่พร้อมใช้งานในแผนปัจจุบันของคุณ",
|
||||
"upgrade.agentRestore.title": "อัปเกรดเพื่อกู้คืนเวอร์ชันแอปเอเจนต์",
|
||||
"upgrade.uploadMultipleFiles.description": "อัปโหลดเอกสารหลายไฟล์พร้อมกัน และเพิ่มขนาดสูงสุดต่อไฟล์เป็น 50 MB",
|
||||
"upgrade.uploadMultipleFiles.title": "อัปเกรดเพื่อปลดล็อกการอัปโหลดแบบเป็นชุดและไฟล์ขนาดใหญ่ขึ้น",
|
||||
"upgrade.uploadMultiplePages.description": "คุณได้ถึงขีดจำกัดการอัปโหลดแล้ว — สามารถเลือกและอัปโหลดเอกสารได้เพียงไฟล์เดียวต่อครั้งในแผนปัจจุบันของคุณ",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "TETİKLEYİCİ OLAYLAR",
|
||||
"upgrade.addChunks.description": "Bu plan için parça ekleme sınırına ulaştınız.",
|
||||
"upgrade.addChunks.title": "Parçalar eklemeye devam etmek için yükseltin",
|
||||
"upgrade.agentRestore.description": "Ajan uygulaması sürümü geri yükleme mevcut planınızda kullanılamaz.",
|
||||
"upgrade.agentRestore.title": "Ajan uygulaması sürümlerini geri yüklemek için yükseltin",
|
||||
"upgrade.uploadMultipleFiles.description": "Birden fazla belgeyi aynı anda yükleyin ve dosya başına maksimum boyutu 50 MB'a çıkarın.",
|
||||
"upgrade.uploadMultipleFiles.title": "Toplu yüklemelerin ve daha büyük dosyaların kilidini açmak için yükseltin",
|
||||
"upgrade.uploadMultiplePages.description": "Yükleme sınırına ulaştınız — mevcut planınızda aynı anda yalnızca bir belge seçip yükleyebilirsiniz.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "ПОДІЇ-ТРИГЕРИ",
|
||||
"upgrade.addChunks.description": "Ви досягли межі додавання фрагментів для цього плану.",
|
||||
"upgrade.addChunks.title": "Оновіть, щоб продовжити додавати частини",
|
||||
"upgrade.agentRestore.description": "Відновлення версій застосунку агента недоступне у вашому поточному плані.",
|
||||
"upgrade.agentRestore.title": "Оновіть план, щоб відновлювати версії застосунку агента",
|
||||
"upgrade.uploadMultipleFiles.description": "Завантажуйте кілька документів одночасно та збільште максимальний розмір одного файлу до 50 МБ.",
|
||||
"upgrade.uploadMultipleFiles.title": "Перейдіть на вищий тариф, щоб розблокувати пакетне завантаження та більші файли",
|
||||
"upgrade.uploadMultiplePages.description": "Ви досягли ліміту завантаження — на вашому поточному плані можна вибрати та завантажити лише один документ одночасно.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "SỰ KIỆN KÍCH HOẠT",
|
||||
"upgrade.addChunks.description": "Bạn đã đạt đến giới hạn thêm phần cho gói này.",
|
||||
"upgrade.addChunks.title": "Nâng cấp để tiếp tục thêm các phần",
|
||||
"upgrade.agentRestore.description": "Tính năng khôi phục phiên bản ứng dụng tác tử không khả dụng trong gói hiện tại của bạn.",
|
||||
"upgrade.agentRestore.title": "Nâng cấp để khôi phục các phiên bản ứng dụng tác tử",
|
||||
"upgrade.uploadMultipleFiles.description": "Tải lên nhiều tài liệu cùng lúc và tăng kích thước tối đa cho mỗi tệp lên 50 MB.",
|
||||
"upgrade.uploadMultipleFiles.title": "Nâng cấp để mở khóa tính năng tải lên hàng loạt và tệp có dung lượng lớn hơn",
|
||||
"upgrade.uploadMultiplePages.description": "Bạn đã đạt đến giới hạn tải lên — chỉ có thể chọn và tải lên một tài liệu trong một lần với gói hiện tại của bạn.",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "触发事件额度",
|
||||
"upgrade.addChunks.description": "您已达到此计划的添加分段上限。",
|
||||
"upgrade.addChunks.title": "升级以继续添加分段",
|
||||
"upgrade.agentRestore.description": "当前套餐不支持恢复 Agent 应用版本。",
|
||||
"upgrade.agentRestore.title": "升级以恢复 Agent 应用版本",
|
||||
"upgrade.uploadMultipleFiles.description": "一次可上传多个文档,单个文件大小上限也将提升至 50 MB。",
|
||||
"upgrade.uploadMultipleFiles.title": "升级以解锁批量上传和更大的文件",
|
||||
"upgrade.uploadMultiplePages.description": "您已达到当前套餐的上传限制 —— 该套餐每次只能选择并上传 1 个文档。",
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
"triggerLimitModal.usageTitle": "觸發事件",
|
||||
"upgrade.addChunks.description": "您已達到此方案可新增區塊的上限。",
|
||||
"upgrade.addChunks.title": "升級以繼續添加區塊",
|
||||
"upgrade.agentRestore.description": "目前方案不支援還原 Agent 應用程式版本。",
|
||||
"upgrade.agentRestore.title": "升級以還原 Agent 應用程式版本",
|
||||
"upgrade.uploadMultipleFiles.description": "一次上傳多份文件,並將單一檔案的大小上限提高至 50 MB。",
|
||||
"upgrade.uploadMultipleFiles.title": "升級方案,解鎖批次上傳與更大檔案",
|
||||
"upgrade.uploadMultiplePages.description": "您已達到上傳限制 — 在您目前的方案下,每次只能選擇並上傳一個文件。",
|
||||
|
||||
Reference in New Issue
Block a user