mirror of
https://github.com/langgenius/dify.git
synced 2026-06-18 07:41:09 +08:00
chore: workflow restore sandbox upgrade (#37568)
This commit is contained in:
parent
912c0fa8d1
commit
e6a91bfcde
@ -1,5 +1,6 @@
|
||||
import type { VersionHistory } from '@/types/workflow'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { renderWorkflowComponent } from '../../__tests__/workflow-test-env'
|
||||
import { WorkflowVersion } from '../../types'
|
||||
@ -10,6 +11,15 @@ const mockInvalidAllLastRun = vi.fn()
|
||||
const mockResetWorkflowVersionHistory = vi.fn()
|
||||
const mockHandleLoadBackupDraft = vi.fn()
|
||||
const mockHandleRefreshWorkflowDraft = vi.fn()
|
||||
let mockPlanType = Plan.professional
|
||||
let mockEnableBilling = true
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
plan: { type: mockPlanType },
|
||||
enableBilling: mockEnableBilling,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({
|
||||
@ -75,6 +85,8 @@ const createVersion = (overrides: Partial<VersionHistory> = {}): VersionHistory
|
||||
describe('HeaderInRestoring', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPlanType = Plan.professional
|
||||
mockEnableBilling = true
|
||||
})
|
||||
|
||||
it('should disable restore when the flow id is not ready yet', () => {
|
||||
@ -125,4 +137,26 @@ describe('HeaderInRestoring', () => {
|
||||
|
||||
expect(screen.getByRole('button', { name: 'workflow.common.restore' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should show plan upgrade modal instead of restoring when sandbox users click restore', () => {
|
||||
mockPlanType = Plan.sandbox
|
||||
renderWorkflowComponent(<HeaderInRestoring />, {
|
||||
initialStoreState: {
|
||||
currentVersion: createVersion(),
|
||||
},
|
||||
hooksStoreProps: {
|
||||
configsMap: {
|
||||
flowId: 'app-1',
|
||||
flowType: FlowType.appFlow,
|
||||
fileSettings: {} as never,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'workflow.common.restore' }))
|
||||
|
||||
expect(screen.getByText('billing.upgrade.workflowRestore.title')).toBeInTheDocument()
|
||||
expect(mockRestoreWorkflow).not.toHaveBeenCalled()
|
||||
expect(mockHandleRefreshWorkflowDraft).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,9 +4,13 @@ import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiHistoryLine } from '@remixicon/react'
|
||||
import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useInvalidAllLastRun, useResetWorkflowVersionHistory, useRestoreWorkflow } from '@/service/use-workflow'
|
||||
import { FlowType } from '@/types/common'
|
||||
@ -32,6 +36,8 @@ const HeaderInRestoring = ({
|
||||
}: HeaderInRestoringProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const [isRestorePlanUpgradeModalOpen, setIsRestorePlanUpgradeModalOpen] = useState(false)
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const userProfile = useAppContextSelector(s => s.userProfile)
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
@ -49,6 +55,7 @@ const HeaderInRestoring = ({
|
||||
const { mutateAsync: restoreWorkflow } = useRestoreWorkflow()
|
||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
|
||||
const canRestore = !!currentVersion?.id && !!configsMap?.flowId && currentVersion.version !== WorkflowVersion.Draft
|
||||
const canUseWorkflowVersionAction = !enableBilling || plan.type !== Plan.sandbox
|
||||
const canEmitCollaborationEvents = configsMap?.flowType === FlowType.appFlow
|
||||
|
||||
const handleCancelRestore = useCallback(() => {
|
||||
@ -116,6 +123,11 @@ const HeaderInRestoring = ({
|
||||
if (!canRestore || !currentVersion)
|
||||
return
|
||||
|
||||
if (!canUseWorkflowVersionAction) {
|
||||
setIsRestorePlanUpgradeModalOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
setShowWorkflowVersionHistoryPanel(false)
|
||||
await emitRestoreIntent()
|
||||
|
||||
@ -138,7 +150,7 @@ const HeaderInRestoring = ({
|
||||
resetWorkflowVersionHistory()
|
||||
onRestoreSettled?.()
|
||||
}
|
||||
}, [canRestore, currentVersion, setShowWorkflowVersionHistoryPanel, emitRestoreIntent, restoreWorkflow, restoreVersionUrl, workflowStore, handleRefreshWorkflowDraft, t, deleteAllInspectVars, invalidAllLastRun, emitRestoreComplete, emitWorkflowUpdate, resetWorkflowVersionHistory, onRestoreSettled])
|
||||
}, [canRestore, currentVersion, canUseWorkflowVersionAction, setShowWorkflowVersionHistoryPanel, emitRestoreIntent, restoreWorkflow, restoreVersionUrl, workflowStore, handleRefreshWorkflowDraft, t, deleteAllInspectVars, invalidAllLastRun, emitRestoreComplete, emitWorkflowUpdate, resetWorkflowVersionHistory, onRestoreSettled])
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -170,6 +182,14 @@ const HeaderInRestoring = ({
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{isRestorePlanUpgradeModalOpen && (
|
||||
<PlanUpgradeModal
|
||||
show
|
||||
onClose={() => setIsRestorePlanUpgradeModalOpen(false)}
|
||||
title={t('upgrade.workflowRestore.title', { ns: 'billing' })!}
|
||||
description={t('upgrade.workflowRestore.description', { ns: 'billing' })!}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,16 +1,23 @@
|
||||
import type { Shape } from '../../../store'
|
||||
import type { VersionHistory } from '@/types/workflow'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { VersionHistoryContextMenuOptions, WorkflowVersion } from '../../../types'
|
||||
|
||||
const mockHandleRestoreFromPublishedWorkflow = vi.fn()
|
||||
const mockHandleLoadBackupDraft = vi.fn()
|
||||
const mockHandleRefreshWorkflowDraft = vi.fn()
|
||||
const mockHandleExportDSL = vi.fn()
|
||||
const mockRestoreWorkflow = vi.fn()
|
||||
const mockSetCurrentVersion = vi.fn()
|
||||
const mockSetShowWorkflowVersionHistoryPanel = vi.fn()
|
||||
const mockWorkflowStoreSetState = vi.fn()
|
||||
const mockEmitRestoreIntent = vi.fn()
|
||||
const mockEmitRestoreComplete = vi.fn()
|
||||
const mockEmitWorkflowUpdate = vi.fn()
|
||||
let mockPlanType = Plan.professional
|
||||
let mockEnableBilling = true
|
||||
|
||||
const createVersionHistory = (overrides: Partial<VersionHistory> = {}): VersionHistory => ({
|
||||
id: 'version-id',
|
||||
@ -56,6 +63,13 @@ vi.mock('@/context/app-context', () => ({
|
||||
useSelector: () => ({ id: 'test-user-id' }),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
plan: { type: mockPlanType },
|
||||
enableBilling: mockEnableBilling,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useDeleteWorkflow: () => ({ mutateAsync: vi.fn() }),
|
||||
useInvalidAllLastRun: () => vi.fn(),
|
||||
@ -88,7 +102,7 @@ vi.mock('@/service/use-workflow', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../../../hooks', () => ({
|
||||
useDSL: () => ({ handleExportDSL: vi.fn() }),
|
||||
useDSL: () => ({ handleExportDSL: mockHandleExportDSL }),
|
||||
useWorkflowRefreshDraft: () => ({ handleRefreshWorkflowDraft: mockHandleRefreshWorkflowDraft }),
|
||||
useWorkflowRun: () => ({
|
||||
handleRestoreFromPublishedWorkflow: mockHandleRestoreFromPublishedWorkflow,
|
||||
@ -103,6 +117,14 @@ vi.mock('../../../hooks-store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../collaboration/core/collaboration-manager', () => ({
|
||||
collaborationManager: {
|
||||
emitRestoreIntent: mockEmitRestoreIntent,
|
||||
emitRestoreComplete: mockEmitRestoreComplete,
|
||||
emitWorkflowUpdate: mockEmitWorkflowUpdate,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../../store', () => ({
|
||||
useStore: <T,>(selector: (state: MockVersionStoreState) => T) => {
|
||||
const state: MockVersionStoreState = {
|
||||
@ -149,19 +171,27 @@ vi.mock('../version-history-item', () => ({
|
||||
default: (props: MockVersionHistoryItemProps) => {
|
||||
const MockVersionHistoryItem = () => {
|
||||
const { item, onClick, handleClickActionMenuItem } = props
|
||||
const didSelectDraftRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (item.version === WorkflowVersion.Draft)
|
||||
if (item.version === WorkflowVersion.Draft && !didSelectDraftRef.current) {
|
||||
didSelectDraftRef.current = true
|
||||
onClick(item)
|
||||
}
|
||||
}, [item, onClick])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => onClick(item)}>{item.marked_name || item.version}</button>
|
||||
{item.version !== WorkflowVersion.Draft && (
|
||||
<button onClick={() => handleClickActionMenuItem(VersionHistoryContextMenuOptions.restore)}>
|
||||
{`restore-${item.id}`}
|
||||
</button>
|
||||
<>
|
||||
<button onClick={() => handleClickActionMenuItem(VersionHistoryContextMenuOptions.restore)}>
|
||||
{`restore-${item.id}`}
|
||||
</button>
|
||||
<button onClick={() => handleClickActionMenuItem(VersionHistoryContextMenuOptions.exportDSL)}>
|
||||
{`export-${item.id}`}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@ -174,7 +204,10 @@ vi.mock('../version-history-item', () => ({
|
||||
describe('VersionHistoryPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockRestoreWorkflow.mockResolvedValue(undefined)
|
||||
mockCurrentVersion = null
|
||||
mockPlanType = Plan.professional
|
||||
mockEnableBilling = true
|
||||
})
|
||||
|
||||
describe('Version Click Behavior', () => {
|
||||
@ -221,6 +254,9 @@ describe('VersionHistoryPanel', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleLoadBackupDraft).toHaveBeenCalled()
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
|
||||
fireEvent.click(screen.getByText('restore-published-version-id'))
|
||||
@ -237,9 +273,47 @@ describe('VersionHistoryPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should show plan upgrade modal instead of restore confirmation for sandbox users', async () => {
|
||||
const { VersionHistoryPanel } = await import('../index')
|
||||
mockPlanType = Plan.sandbox
|
||||
|
||||
render(
|
||||
<VersionHistoryPanel
|
||||
latestVersionId="published-version-id"
|
||||
restoreVersionUrl={versionId => `/apps/app-1/workflows/${versionId}/restore`}
|
||||
/>,
|
||||
)
|
||||
|
||||
vi.clearAllMocks()
|
||||
|
||||
fireEvent.click(screen.getByText('restore-published-version-id'))
|
||||
|
||||
expect(screen.getByText('billing.upgrade.workflowRestore.title')).toBeInTheDocument()
|
||||
expect(screen.queryByText('confirm restore')).not.toBeInTheDocument()
|
||||
expect(mockRestoreWorkflow).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show plan upgrade modal instead of exporting DSL for sandbox users', async () => {
|
||||
const { VersionHistoryPanel } = await import('../index')
|
||||
mockPlanType = Plan.sandbox
|
||||
|
||||
render(
|
||||
<VersionHistoryPanel
|
||||
latestVersionId="published-version-id"
|
||||
restoreVersionUrl={versionId => `/apps/app-1/workflows/${versionId}/restore`}
|
||||
/>,
|
||||
)
|
||||
|
||||
vi.clearAllMocks()
|
||||
|
||||
fireEvent.click(screen.getByText('export-published-version-id'))
|
||||
|
||||
expect(screen.getByText('billing.upgrade.workflowRestore.title')).toBeInTheDocument()
|
||||
expect(mockHandleExportDSL).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should keep restore mode backup state when restore request fails', async () => {
|
||||
const { VersionHistoryPanel } = await import('../index')
|
||||
mockRestoreWorkflow.mockRejectedValueOnce(new Error('restore failed'))
|
||||
mockCurrentVersion = createVersionHistory({
|
||||
id: 'draft-version-id',
|
||||
version: WorkflowVersion.Draft,
|
||||
@ -253,6 +327,7 @@ describe('VersionHistoryPanel', () => {
|
||||
)
|
||||
|
||||
vi.clearAllMocks()
|
||||
mockRestoreWorkflow.mockRejectedValueOnce(new Error('restore failed'))
|
||||
|
||||
fireEvent.click(screen.getByText('restore-published-version-id'))
|
||||
fireEvent.click(screen.getByText('confirm restore'))
|
||||
|
||||
@ -1,10 +1,35 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { renderWorkflowComponent } from '../../../../__tests__/workflow-test-env'
|
||||
import { VersionHistoryContextMenuOptions } from '../../../../types'
|
||||
import ActionMenu from '../index'
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
return {
|
||||
...actual,
|
||||
IS_CLOUD_EDITION: true,
|
||||
}
|
||||
})
|
||||
|
||||
let mockPlanType = Plan.professional
|
||||
let mockEnableBilling = true
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
plan: { type: mockPlanType },
|
||||
enableBilling: mockEnableBilling,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('ActionMenu', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPlanType = Plan.professional
|
||||
mockEnableBilling = true
|
||||
})
|
||||
|
||||
it('toggles the trigger and forwards menu clicks', async () => {
|
||||
const user = userEvent.setup()
|
||||
const setOpen = vi.fn()
|
||||
@ -34,4 +59,27 @@ describe('ActionMenu', () => {
|
||||
VersionHistoryContextMenuOptions.delete,
|
||||
)
|
||||
})
|
||||
|
||||
it('shows upgrade buttons beside restore and export for sandbox users', async () => {
|
||||
const user = userEvent.setup()
|
||||
const handleClickActionMenuItem = vi.fn()
|
||||
mockPlanType = Plan.sandbox
|
||||
|
||||
renderWorkflowComponent(
|
||||
<ActionMenu
|
||||
isNamedVersion
|
||||
isShowDelete
|
||||
open
|
||||
setOpen={vi.fn()}
|
||||
handleClickActionMenuItem={handleClickActionMenuItem}
|
||||
/>,
|
||||
)
|
||||
|
||||
const upgradeButtons = screen.getAllByRole('button', { name: 'billing.upgradeBtn.encourageShort' })
|
||||
expect(upgradeButtons).toHaveLength(2)
|
||||
|
||||
await user.click(upgradeButtons[0]!)
|
||||
|
||||
expect(handleClickActionMenuItem).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,11 +3,13 @@ import type { VersionHistoryContextMenuOptions } from '../../../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { DropdownMenuItem } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import * as React from 'react'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
|
||||
type ActionMenuItemProps = {
|
||||
item: {
|
||||
key: VersionHistoryContextMenuOptions
|
||||
name: string
|
||||
showUpgrade?: boolean
|
||||
}
|
||||
onClick: (operation: VersionHistoryContextMenuOptions) => void
|
||||
isDestructive?: boolean
|
||||
@ -22,21 +24,38 @@ const ActionMenuItem: FC<ActionMenuItemProps> = ({
|
||||
<DropdownMenuItem
|
||||
variant={isDestructive ? 'destructive' : 'default'}
|
||||
className={cn(
|
||||
'justify-between px-2 py-1.5',
|
||||
'justify-between gap-x-3 px-2 py-1.5 whitespace-nowrap',
|
||||
isDestructive && 'data-highlighted:bg-state-destructive-hover',
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
const target = event.target
|
||||
if (target instanceof Element && target.closest('[data-upgrade-action]'))
|
||||
return
|
||||
|
||||
onClick(item.key)
|
||||
}}
|
||||
>
|
||||
<div className={cn(
|
||||
'flex-1 system-md-regular text-text-primary',
|
||||
'flex-1 system-md-regular whitespace-nowrap text-text-primary',
|
||||
isDestructive && 'text-inherit',
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
{item.showUpgrade && (
|
||||
<div
|
||||
data-upgrade-action
|
||||
className="shrink-0"
|
||||
>
|
||||
<UpgradeBtn
|
||||
size="custom"
|
||||
isShort
|
||||
loc="workflow-version-history-menu"
|
||||
className="h-5! rounded-md! px-1!"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ const ActionMenu: FC<ActionMenuProps> = (props: ActionMenuProps) => {
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName="w-[184px] shadow-shadow-shadow-5"
|
||||
popupClassName="w-max min-w-[184px] max-w-[calc(100vw-24px)] shadow-shadow-shadow-5"
|
||||
>
|
||||
{
|
||||
options.map(option => (
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import type { ActionMenuProps } from './index'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { VersionHistoryContextMenuOptions } from '../../../types'
|
||||
|
||||
const useActionMenu = (props: ActionMenuProps) => {
|
||||
@ -10,6 +12,8 @@ const useActionMenu = (props: ActionMenuProps) => {
|
||||
} = props
|
||||
const { t } = useTranslation()
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const shouldShowUpgrade = enableBilling && plan.type === Plan.sandbox
|
||||
|
||||
const deleteOperation = {
|
||||
key: VersionHistoryContextMenuOptions.delete,
|
||||
@ -21,6 +25,7 @@ const useActionMenu = (props: ActionMenuProps) => {
|
||||
{
|
||||
key: VersionHistoryContextMenuOptions.restore,
|
||||
name: t('common.restore', { ns: 'workflow' }),
|
||||
...(shouldShowUpgrade ? { showUpgrade: true } : {}),
|
||||
},
|
||||
isNamedVersion
|
||||
? {
|
||||
@ -36,6 +41,7 @@ const useActionMenu = (props: ActionMenuProps) => {
|
||||
? [{
|
||||
key: VersionHistoryContextMenuOptions.exportDSL,
|
||||
name: t('export', { ns: 'app' }),
|
||||
...(shouldShowUpgrade ? { showUpgrade: true } : {}),
|
||||
}]
|
||||
: []),
|
||||
{
|
||||
@ -43,7 +49,7 @@ const useActionMenu = (props: ActionMenuProps) => {
|
||||
name: t('versionHistory.copyId', { ns: 'workflow' }),
|
||||
},
|
||||
]
|
||||
}, [isNamedVersion, pipelineId, t])
|
||||
}, [isNamedVersion, pipelineId, shouldShowUpgrade, t])
|
||||
|
||||
return {
|
||||
deleteOperation,
|
||||
|
||||
@ -8,7 +8,10 @@ import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VersionInfoModal from '@/app/components/app/app-publisher/version-info-modal'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useDeleteWorkflow, useInvalidAllLastRun, useResetWorkflowVersionHistory, useRestoreWorkflow, useUpdateWorkflow, useWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
import { useDSL, useWorkflowRefreshDraft, useWorkflowRun } from '../../hooks'
|
||||
import { useHooksStore } from '../../hooks-store'
|
||||
@ -43,8 +46,11 @@ export const VersionHistoryPanel = ({
|
||||
const [isOnlyShowNamedVersions, setIsOnlyShowNamedVersions] = useState(false)
|
||||
const [operatedItem, setOperatedItem] = useState<VersionHistory>()
|
||||
const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false)
|
||||
const [isRestorePlanUpgradeModalOpen, setIsRestorePlanUpgradeModalOpen] = useState(false)
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
|
||||
const [editModalOpen, setEditModalOpen] = useState(false)
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const canUseWorkflowVersionAction = !enableBilling || plan.type !== Plan.sandbox
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleRestoreFromPublishedWorkflow, handleLoadBackupDraft } = useWorkflowRun()
|
||||
const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft()
|
||||
@ -111,6 +117,10 @@ export const VersionHistoryPanel = ({
|
||||
setOperatedItem(item)
|
||||
switch (operation) {
|
||||
case VersionHistoryContextMenuOptions.restore:
|
||||
if (!canUseWorkflowVersionAction) {
|
||||
setIsRestorePlanUpgradeModalOpen(true)
|
||||
break
|
||||
}
|
||||
setRestoreConfirmOpen(true)
|
||||
break
|
||||
case VersionHistoryContextMenuOptions.edit:
|
||||
@ -124,10 +134,14 @@ export const VersionHistoryPanel = ({
|
||||
toast.success(t('versionHistory.action.copyIdSuccess', { ns: 'workflow' }))
|
||||
break
|
||||
case VersionHistoryContextMenuOptions.exportDSL:
|
||||
if (!canUseWorkflowVersionAction) {
|
||||
setIsRestorePlanUpgradeModalOpen(true)
|
||||
break
|
||||
}
|
||||
handleExportDSL?.(false, item.id)
|
||||
break
|
||||
}
|
||||
}, [t, handleExportDSL])
|
||||
}, [canUseWorkflowVersionAction, t, handleExportDSL])
|
||||
|
||||
const handleCancel = useCallback((operation: VersionHistoryContextMenuOptions) => {
|
||||
switch (operation) {
|
||||
@ -330,6 +344,14 @@ export const VersionHistoryPanel = ({
|
||||
onRestore={handleRestore}
|
||||
/>
|
||||
)}
|
||||
{isRestorePlanUpgradeModalOpen && (
|
||||
<PlanUpgradeModal
|
||||
show
|
||||
onClose={() => setIsRestorePlanUpgradeModalOpen(false)}
|
||||
title={t('upgrade.workflowRestore.title', { ns: 'billing' })!}
|
||||
description={t('upgrade.workflowRestore.description', { ns: 'billing' })!}
|
||||
/>
|
||||
)}
|
||||
{deleteConfirmOpen && (
|
||||
<DeleteConfirmModal
|
||||
isOpen={deleteConfirmOpen}
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "قم بالترقية لفتح ميزة تحميل المستندات دفعة واحدة",
|
||||
"upgrade.uploadMultiplePages.description": "لقد وصلت إلى حد التحميل — يمكن اختيار ورفع مستند واحد فقط في كل مرة على الخطة الحالية الخاصة بك.",
|
||||
"upgrade.uploadMultiplePages.title": "قم بالترقية لتحميل عدة مستندات دفعة واحدة",
|
||||
"upgrade.workflowRestore.description": "استعادة إصدارات سير العمل غير متاحة في خطتك الحالية.",
|
||||
"upgrade.workflowRestore.title": "قم بالترقية لاستعادة إصدارات سير العمل",
|
||||
"upgradeBtn.encourage": "الترقية الآن",
|
||||
"upgradeBtn.encourageShort": "ترقية",
|
||||
"upgradeBtn.plain": "عرض الخطة",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Upgrade, um den Massen-Upload von Dokumenten freizuschalten",
|
||||
"upgrade.uploadMultiplePages.description": "Sie haben das Upload-Limit erreicht – in Ihrem aktuellen Tarif kann jeweils nur ein Dokument ausgewählt und hochgeladen werden.",
|
||||
"upgrade.uploadMultiplePages.title": "Upgrade, um mehrere Dokumente gleichzeitig hochzuladen",
|
||||
"upgrade.workflowRestore.description": "Die Wiederherstellung von Workflow-Versionen ist in Ihrem aktuellen Tarif nicht verfügbar.",
|
||||
"upgrade.workflowRestore.title": "Upgraden, um Workflow-Versionen wiederherzustellen",
|
||||
"upgradeBtn.encourage": "Jetzt Upgraden",
|
||||
"upgradeBtn.encourageShort": "Upgraden",
|
||||
"upgradeBtn.plain": "Tarif Upgraden",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Upgrade to unlock batch document upload",
|
||||
"upgrade.uploadMultiplePages.description": "You’ve reached the upload limit — only one document can be selected and uploaded at a time on your current plan.",
|
||||
"upgrade.uploadMultiplePages.title": "Upgrade to upload multiple documents at once",
|
||||
"upgrade.workflowRestore.description": "Workflow version restoration is not available on your current plan.",
|
||||
"upgrade.workflowRestore.title": "Upgrade to restore workflow versions",
|
||||
"upgradeBtn.encourage": "Upgrade Now",
|
||||
"upgradeBtn.encourageShort": "Upgrade",
|
||||
"upgradeBtn.plain": "View Plan",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Actualiza para desbloquear la carga de documentos en lote",
|
||||
"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.",
|
||||
"upgrade.uploadMultiplePages.title": "Actualiza para subir varios documentos a la vez",
|
||||
"upgrade.workflowRestore.description": "La restauración de versiones del flujo de trabajo no está disponible en tu plan actual.",
|
||||
"upgrade.workflowRestore.title": "Actualiza para restaurar versiones del flujo de trabajo",
|
||||
"upgradeBtn.encourage": "Actualizar Ahora",
|
||||
"upgradeBtn.encourageShort": "Actualizar",
|
||||
"upgradeBtn.plain": "Actualizar Plan",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "ارتقا دهید تا امکان بارگذاری دستهای اسناد فعال شود",
|
||||
"upgrade.uploadMultiplePages.description": "شما به حد آپلود رسیدهاید — در طرح فعلی خود تنها میتوانید یک سند را در هر بار انتخاب و آپلود کنید.",
|
||||
"upgrade.uploadMultiplePages.title": "ارتقا برای آپلود همزمان چندین سند",
|
||||
"upgrade.workflowRestore.description": "بازیابی نسخههای گردشکار در طرح فعلی شما در دسترس نیست.",
|
||||
"upgrade.workflowRestore.title": "برای بازیابی نسخههای گردشکار ارتقا دهید",
|
||||
"upgradeBtn.encourage": "هم اکنون ارتقاء دهید",
|
||||
"upgradeBtn.encourageShort": "ارتقاء دهید",
|
||||
"upgradeBtn.plain": "ارتقاء طرح",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Passez à la version supérieure pour débloquer le téléchargement de documents en lot",
|
||||
"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.",
|
||||
"upgrade.uploadMultiplePages.title": "Passez à la version supérieure pour télécharger plusieurs documents à la fois",
|
||||
"upgrade.workflowRestore.description": "La restauration des versions du workflow n'est pas disponible avec votre plan actuel.",
|
||||
"upgrade.workflowRestore.title": "Mettez à niveau pour restaurer les versions du workflow",
|
||||
"upgradeBtn.encourage": "Mettre à niveau maintenant",
|
||||
"upgradeBtn.encourageShort": "Mise à niveau",
|
||||
"upgradeBtn.plain": "Mettre à jour le plan",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "बैच दस्तावेज़ अपलोड अनलॉक करने के लिए अपग्रेड करें",
|
||||
"upgrade.uploadMultiplePages.description": "आपने अपलोड की सीमा तक पहुँच लिया है — आपके वर्तमान प्लान पर एक समय में केवल एक ही दस्तावेज़ चुना और अपलोड किया जा सकता है।",
|
||||
"upgrade.uploadMultiplePages.title": "एक बार में कई दस्तावेज़ अपलोड करने के लिए अपग्रेड करें",
|
||||
"upgrade.workflowRestore.description": "आपकी वर्तमान योजना में वर्कफ़्लो संस्करण पुनर्स्थापना उपलब्ध नहीं है।",
|
||||
"upgrade.workflowRestore.title": "वर्कफ़्लो संस्करणों को पुनर्स्थापित करने के लिए अपग्रेड करें",
|
||||
"upgradeBtn.encourage": "अभी अपग्रेड करें",
|
||||
"upgradeBtn.encourageShort": "अपग्रेड करें",
|
||||
"upgradeBtn.plain": "योजना अपग्रेड करें",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Tingkatkan untuk membuka unggahan dokumen batch",
|
||||
"upgrade.uploadMultiplePages.description": "Anda telah mencapai batas unggah — hanya satu dokumen yang dapat dipilih dan diunggah sekaligus dengan paket Anda saat ini.",
|
||||
"upgrade.uploadMultiplePages.title": "Tingkatkan untuk mengunggah beberapa dokumen sekaligus",
|
||||
"upgrade.workflowRestore.description": "Pemulihan versi workflow tidak tersedia di paket Anda saat ini.",
|
||||
"upgrade.workflowRestore.title": "Tingkatkan untuk memulihkan versi workflow",
|
||||
"upgradeBtn.encourage": "Tingkatkan Sekarang",
|
||||
"upgradeBtn.encourageShort": "Upgrade",
|
||||
"upgradeBtn.plain": "Lihat Paket",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Aggiorna per sbloccare il caricamento di documenti in batch",
|
||||
"upgrade.uploadMultiplePages.description": "Hai raggiunto il limite di caricamento: sul tuo piano attuale può essere selezionato e caricato un solo documento alla volta.",
|
||||
"upgrade.uploadMultiplePages.title": "Aggiorna per caricare più documenti contemporaneamente",
|
||||
"upgrade.workflowRestore.description": "Il ripristino delle versioni del workflow non è disponibile nel tuo piano attuale.",
|
||||
"upgrade.workflowRestore.title": "Esegui l'upgrade per ripristinare le versioni del workflow",
|
||||
"upgradeBtn.encourage": "Aggiorna Ora",
|
||||
"upgradeBtn.encourageShort": "Aggiorna",
|
||||
"upgradeBtn.plain": "Aggiorna Piano",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "一括ドキュメントアップロード機能を解放するにはアップグレードが必要です",
|
||||
"upgrade.uploadMultiplePages.description": "現在のプランではアップロード上限に達しています。1回の操作で選択・アップロードできるドキュメントは1つのみです。",
|
||||
"upgrade.uploadMultiplePages.title": "複数ドキュメントを一度にアップロードするにはアップグレード",
|
||||
"upgrade.workflowRestore.description": "現在のプランでは、ワークフローバージョンの復元は利用できません。",
|
||||
"upgrade.workflowRestore.title": "アップグレードしてワークフローバージョンを復元",
|
||||
"upgradeBtn.encourage": "今すぐアップグレード",
|
||||
"upgradeBtn.encourageShort": "アップグレード",
|
||||
"upgradeBtn.plain": "プランを見る",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "업그레이드하여 대량 문서 업로드 기능 잠금 해제",
|
||||
"upgrade.uploadMultiplePages.description": "업로드 한도에 도달했습니다 — 현재 요금제에서는 한 번에 한 개의 문서만 선택하고 업로드할 수 있습니다.",
|
||||
"upgrade.uploadMultiplePages.title": "한 번에 여러 문서를 업로드하려면 업그레이드하세요",
|
||||
"upgrade.workflowRestore.description": "현재 플랜에서는 워크플로 버전 복원을 사용할 수 없습니다.",
|
||||
"upgrade.workflowRestore.title": "워크플로 버전을 복원하려면 업그레이드하세요",
|
||||
"upgradeBtn.encourage": "지금 업그레이드",
|
||||
"upgradeBtn.encourageShort": "업그레이드",
|
||||
"upgradeBtn.plain": "요금제 업그레이드",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Upgrade to unlock batch document upload",
|
||||
"upgrade.uploadMultiplePages.description": "You’ve reached the upload limit — only one document can be selected and uploaded at a time on your current plan.",
|
||||
"upgrade.uploadMultiplePages.title": "Upgrade to upload multiple documents at once",
|
||||
"upgrade.workflowRestore.description": "Het herstellen van workflowversies is niet beschikbaar in je huidige abonnement.",
|
||||
"upgrade.workflowRestore.title": "Upgrade om workflowversies te herstellen",
|
||||
"upgradeBtn.encourage": "Upgrade Now",
|
||||
"upgradeBtn.encourageShort": "Upgrade",
|
||||
"upgradeBtn.plain": "View Plan",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Uaktualnij, aby odblokować przesyłanie dokumentów wsadowych",
|
||||
"upgrade.uploadMultiplePages.description": "Osiągnąłeś limit przesyłania — w ramach obecnego planu można wybrać i przesłać tylko jeden dokument naraz.",
|
||||
"upgrade.uploadMultiplePages.title": "Przejdź na wyższą wersję, aby przesyłać wiele dokumentów jednocześnie",
|
||||
"upgrade.workflowRestore.description": "Przywracanie wersji przepływu pracy nie jest dostępne w obecnym planie.",
|
||||
"upgrade.workflowRestore.title": "Ulepsz plan, aby przywracać wersje przepływu pracy",
|
||||
"upgradeBtn.encourage": "Ulepsz teraz",
|
||||
"upgradeBtn.encourageShort": "Ulepsz",
|
||||
"upgradeBtn.plain": "Ulepsz plan",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Atualize para desbloquear o envio de documentos em lote",
|
||||
"upgrade.uploadMultiplePages.description": "Você atingiu o limite de upload — apenas um documento pode ser selecionado e enviado por vez no seu plano atual.",
|
||||
"upgrade.uploadMultiplePages.title": "Atualize para enviar vários documentos de uma vez",
|
||||
"upgrade.workflowRestore.description": "A restauração de versões do fluxo de trabalho não está disponível no seu plano atual.",
|
||||
"upgrade.workflowRestore.title": "Faça upgrade para restaurar versões do fluxo de trabalho",
|
||||
"upgradeBtn.encourage": "Fazer Upgrade Agora",
|
||||
"upgradeBtn.encourageShort": "Upgrade",
|
||||
"upgradeBtn.plain": "Fazer Upgrade do Plano",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Fă upgrade pentru a debloca încărcarea documentelor în masă",
|
||||
"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.",
|
||||
"upgrade.uploadMultiplePages.title": "Actualizează pentru a încărca mai multe documente odată",
|
||||
"upgrade.workflowRestore.description": "Restaurarea versiunilor fluxului de lucru nu este disponibilă în planul tău actual.",
|
||||
"upgrade.workflowRestore.title": "Fă upgrade pentru a restaura versiunile fluxului de lucru",
|
||||
"upgradeBtn.encourage": "Actualizează acum",
|
||||
"upgradeBtn.encourageShort": "Actualizează",
|
||||
"upgradeBtn.plain": "Actualizează planul",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Обновите версию, чтобы включить массовую загрузку документов",
|
||||
"upgrade.uploadMultiplePages.description": "Вы достигли лимита загрузки — на вашем текущем тарифном плане можно выбрать и загрузить только один документ за раз.",
|
||||
"upgrade.uploadMultiplePages.title": "Обновите версию, чтобы загружать несколько документов одновременно",
|
||||
"upgrade.workflowRestore.description": "Восстановление версий рабочего процесса недоступно в вашем текущем плане.",
|
||||
"upgrade.workflowRestore.title": "Обновите план, чтобы восстанавливать версии рабочего процесса",
|
||||
"upgradeBtn.encourage": "Обновить сейчас",
|
||||
"upgradeBtn.encourageShort": "Обновить",
|
||||
"upgradeBtn.plain": "Обновить тарифный план",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Nadgradite za odklep nalaganja dokumentov v skupkih",
|
||||
"upgrade.uploadMultiplePages.description": "Dosegli ste omejitev nalaganja — na vašem trenutnem načrtu je mogoče izbrati in naložiti le en dokument naenkrat.",
|
||||
"upgrade.uploadMultiplePages.title": "Nadgradite za nalaganje več dokumentov hkrati",
|
||||
"upgrade.workflowRestore.description": "Obnovitev različic poteka dela v vašem trenutnem paketu ni na voljo.",
|
||||
"upgrade.workflowRestore.title": "Nadgradite za obnovitev različic poteka dela",
|
||||
"upgradeBtn.encourage": "Nadgradi zdaj",
|
||||
"upgradeBtn.encourageShort": "Nadgradi",
|
||||
"upgradeBtn.plain": "Nadgradi načrt",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "อัปเกรดเพื่อปลดล็อกการอัปโหลดเอกสารเป็นชุด",
|
||||
"upgrade.uploadMultiplePages.description": "คุณได้ถึงขีดจำกัดการอัปโหลดแล้ว — สามารถเลือกและอัปโหลดเอกสารได้เพียงไฟล์เดียวต่อครั้งในแผนปัจจุบันของคุณ",
|
||||
"upgrade.uploadMultiplePages.title": "อัปเกรดเพื่ออัปโหลดเอกสารหลายฉบับพร้อมกัน",
|
||||
"upgrade.workflowRestore.description": "การกู้คืนเวอร์ชันเวิร์กโฟลว์ไม่พร้อมใช้งานในแผนปัจจุบันของคุณ",
|
||||
"upgrade.workflowRestore.title": "อัปเกรดเพื่อกู้คืนเวอร์ชันเวิร์กโฟลว์",
|
||||
"upgradeBtn.encourage": "อัปเกรดเดี๋ยวนี้",
|
||||
"upgradeBtn.encourageShort": "อัพ เกรด",
|
||||
"upgradeBtn.plain": "แผนการอัปเกรด",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Toplu belge yüklemeyi 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.",
|
||||
"upgrade.uploadMultiplePages.title": "Aynı anda birden fazla belge yüklemek için yükseltin",
|
||||
"upgrade.workflowRestore.description": "İş akışı sürümü geri yükleme mevcut planınızda kullanılamaz.",
|
||||
"upgrade.workflowRestore.title": "İş akışı sürümlerini geri yüklemek için yükseltin",
|
||||
"upgradeBtn.encourage": "Şimdi Yükselt",
|
||||
"upgradeBtn.encourageShort": "Yükselt",
|
||||
"upgradeBtn.plain": "Planı Yükselt",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Оновіть, щоб розблокувати пакетне завантаження документів",
|
||||
"upgrade.uploadMultiplePages.description": "Ви досягли ліміту завантаження — на вашому поточному плані можна вибрати та завантажити лише один документ одночасно.",
|
||||
"upgrade.uploadMultiplePages.title": "Оновіть, щоб завантажувати кілька документів одночасно",
|
||||
"upgrade.workflowRestore.description": "Відновлення версій робочого процесу недоступне у вашому поточному плані.",
|
||||
"upgrade.workflowRestore.title": "Оновіть план, щоб відновлювати версії робочого процесу",
|
||||
"upgradeBtn.encourage": "Оновити зараз",
|
||||
"upgradeBtn.encourageShort": "Оновити",
|
||||
"upgradeBtn.plain": "Оновити план",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "Nâng cấp để mở khóa tải lên nhiều tài liệu",
|
||||
"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.",
|
||||
"upgrade.uploadMultiplePages.title": "Nâng cấp để tải lên nhiều tài liệu cùng lúc",
|
||||
"upgrade.workflowRestore.description": "Tính năng khôi phục phiên bản quy trình không khả dụng trong gói hiện tại của bạn.",
|
||||
"upgrade.workflowRestore.title": "Nâng cấp để khôi phục các phiên bản quy trình",
|
||||
"upgradeBtn.encourage": "Nâng cấp Ngay",
|
||||
"upgradeBtn.encourageShort": "Nâng cấp",
|
||||
"upgradeBtn.plain": "Nâng cấp Kế hoạch",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "升级以解锁批量文档上传功能",
|
||||
"upgrade.uploadMultiplePages.description": "您已达到当前套餐的上传限制 —— 该套餐每次只能选择并上传 1 个文档。",
|
||||
"upgrade.uploadMultiplePages.title": "升级以一次性上传多个文档",
|
||||
"upgrade.workflowRestore.description": "当前套餐不支持恢复工作流版本。",
|
||||
"upgrade.workflowRestore.title": "升级以恢复工作流版本",
|
||||
"upgradeBtn.encourage": "立即升级",
|
||||
"upgradeBtn.encourageShort": "升级",
|
||||
"upgradeBtn.plain": "查看套餐",
|
||||
|
||||
@ -164,6 +164,8 @@
|
||||
"upgrade.uploadMultipleFiles.title": "升級以解鎖批量上傳文件功能",
|
||||
"upgrade.uploadMultiplePages.description": "您已達到上傳限制 — 在您目前的方案下,每次只能選擇並上傳一個文件。",
|
||||
"upgrade.uploadMultiplePages.title": "升級以一次上傳多個文件",
|
||||
"upgrade.workflowRestore.description": "目前方案不支援還原工作流程版本。",
|
||||
"upgrade.workflowRestore.title": "升級以還原工作流程版本",
|
||||
"upgradeBtn.encourage": "立即升級",
|
||||
"upgradeBtn.encourageShort": "升級",
|
||||
"upgradeBtn.plain": "升級套餐",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user