mirror of https://github.com/langgenius/dify.git
chore: remove frontend changes
This commit is contained in:
parent
f55faae31b
commit
56c8221b3f
|
|
@ -73,3 +73,6 @@ NEXT_PUBLIC_MAX_TREE_DEPTH=50
|
|||
|
||||
# The API key of amplitude
|
||||
NEXT_PUBLIC_AMPLITUDE_API_KEY=
|
||||
|
||||
# number of concurrency
|
||||
NEXT_PUBLIC_BATCH_CONCURRENCY=5
|
||||
|
|
|
|||
|
|
@ -54,3 +54,13 @@ package-lock.json
|
|||
# mise
|
||||
mise.toml
|
||||
|
||||
|
||||
# PWA generated files
|
||||
public/sw.js
|
||||
public/sw.js.map
|
||||
public/workbox-*.js
|
||||
public/workbox-*.js.map
|
||||
public/fallback-*.js
|
||||
|
||||
.vscode/settings.json
|
||||
.vscode/mcp.json
|
||||
|
|
|
|||
|
|
@ -1,144 +0,0 @@
|
|||
{
|
||||
"plugins": [
|
||||
"unicorn",
|
||||
"typescript",
|
||||
"oxc"
|
||||
],
|
||||
"categories": {},
|
||||
"rules": {
|
||||
"for-direction": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-caller": "error",
|
||||
"no-class-assign": "error",
|
||||
"no-compare-neg-zero": "error",
|
||||
"no-cond-assign": "warn",
|
||||
"no-const-assign": "warn",
|
||||
"no-constant-binary-expression": "error",
|
||||
"no-constant-condition": "warn",
|
||||
"no-control-regex": "warn",
|
||||
"no-debugger": "warn",
|
||||
"no-delete-var": "warn",
|
||||
"no-dupe-class-members": "warn",
|
||||
"no-dupe-else-if": "warn",
|
||||
"no-dupe-keys": "warn",
|
||||
"no-duplicate-case": "warn",
|
||||
"no-empty-character-class": "warn",
|
||||
"no-empty-pattern": "warn",
|
||||
"no-empty-static-block": "warn",
|
||||
"no-eval": "warn",
|
||||
"no-ex-assign": "warn",
|
||||
"no-extra-boolean-cast": "warn",
|
||||
"no-func-assign": "warn",
|
||||
"no-global-assign": "warn",
|
||||
"no-import-assign": "warn",
|
||||
"no-invalid-regexp": "warn",
|
||||
"no-irregular-whitespace": "warn",
|
||||
"no-loss-of-precision": "warn",
|
||||
"no-new-native-nonconstructor": "warn",
|
||||
"no-nonoctal-decimal-escape": "warn",
|
||||
"no-obj-calls": "warn",
|
||||
"no-self-assign": "warn",
|
||||
"no-setter-return": "warn",
|
||||
"no-shadow-restricted-names": "warn",
|
||||
"no-sparse-arrays": "warn",
|
||||
"no-this-before-super": "warn",
|
||||
"no-unassigned-vars": "warn",
|
||||
"no-unsafe-finally": "warn",
|
||||
"no-unsafe-negation": "warn",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-unused-labels": "warn",
|
||||
"no-unused-private-class-members": "warn",
|
||||
"no-unused-vars": "warn",
|
||||
"no-useless-backreference": "warn",
|
||||
"no-useless-catch": "error",
|
||||
"no-useless-escape": "warn",
|
||||
"no-useless-rename": "warn",
|
||||
"no-with": "warn",
|
||||
"require-yield": "warn",
|
||||
"use-isnan": "warn",
|
||||
"valid-typeof": "warn",
|
||||
"oxc/bad-array-method-on-arguments": "warn",
|
||||
"oxc/bad-char-at-comparison": "warn",
|
||||
"oxc/bad-comparison-sequence": "warn",
|
||||
"oxc/bad-min-max-func": "warn",
|
||||
"oxc/bad-object-literal-comparison": "warn",
|
||||
"oxc/bad-replace-all-arg": "warn",
|
||||
"oxc/const-comparisons": "warn",
|
||||
"oxc/double-comparisons": "warn",
|
||||
"oxc/erasing-op": "warn",
|
||||
"oxc/missing-throw": "warn",
|
||||
"oxc/number-arg-out-of-range": "warn",
|
||||
"oxc/only-used-in-recursion": "warn",
|
||||
"oxc/uninvoked-array-callback": "warn",
|
||||
"typescript/await-thenable": "warn",
|
||||
"typescript/no-array-delete": "warn",
|
||||
"typescript/no-base-to-string": "warn",
|
||||
"typescript/no-confusing-void-expression": "warn",
|
||||
"typescript/no-duplicate-enum-values": "warn",
|
||||
"typescript/no-duplicate-type-constituents": "warn",
|
||||
"typescript/no-extra-non-null-assertion": "warn",
|
||||
"typescript/no-floating-promises": "warn",
|
||||
"typescript/no-for-in-array": "warn",
|
||||
"typescript/no-implied-eval": "warn",
|
||||
"typescript/no-meaningless-void-operator": "warn",
|
||||
"typescript/no-misused-new": "warn",
|
||||
"typescript/no-misused-spread": "warn",
|
||||
"typescript/no-non-null-asserted-optional-chain": "warn",
|
||||
"typescript/no-redundant-type-constituents": "warn",
|
||||
"typescript/no-this-alias": "warn",
|
||||
"typescript/no-unnecessary-parameter-property-assignment": "warn",
|
||||
"typescript/no-unsafe-declaration-merging": "warn",
|
||||
"typescript/no-unsafe-unary-minus": "warn",
|
||||
"typescript/no-useless-empty-export": "warn",
|
||||
"typescript/no-wrapper-object-types": "warn",
|
||||
"typescript/prefer-as-const": "warn",
|
||||
"typescript/require-array-sort-compare": "warn",
|
||||
"typescript/restrict-template-expressions": "warn",
|
||||
"typescript/triple-slash-reference": "warn",
|
||||
"typescript/unbound-method": "warn",
|
||||
"unicorn/no-await-in-promise-methods": "warn",
|
||||
"unicorn/no-empty-file": "warn",
|
||||
"unicorn/no-invalid-fetch-options": "warn",
|
||||
"unicorn/no-invalid-remove-event-listener": "warn",
|
||||
"unicorn/no-new-array": "warn",
|
||||
"unicorn/no-single-promise-in-promise-methods": "warn",
|
||||
"unicorn/no-thenable": "warn",
|
||||
"unicorn/no-unnecessary-await": "warn",
|
||||
"unicorn/no-useless-fallback-in-spread": "warn",
|
||||
"unicorn/no-useless-length-check": "warn",
|
||||
"unicorn/no-useless-spread": "warn",
|
||||
"unicorn/prefer-set-size": "warn",
|
||||
"unicorn/prefer-string-starts-ends-with": "warn"
|
||||
},
|
||||
"settings": {
|
||||
"jsx-a11y": {
|
||||
"polymorphicPropName": null,
|
||||
"components": {},
|
||||
"attributes": {}
|
||||
},
|
||||
"next": {
|
||||
"rootDir": []
|
||||
},
|
||||
"react": {
|
||||
"formComponents": [],
|
||||
"linkComponents": []
|
||||
},
|
||||
"jsdoc": {
|
||||
"ignorePrivate": false,
|
||||
"ignoreInternal": false,
|
||||
"ignoreReplacesDocs": true,
|
||||
"overrideReplacesDocs": true,
|
||||
"augmentsExtendsReplacesDocs": false,
|
||||
"implementsReplacesDocs": false,
|
||||
"exemptDestructuredRootsFromChecks": false,
|
||||
"tagNamePreference": {}
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"builtin": true
|
||||
},
|
||||
"globals": {},
|
||||
"ignorePatterns": [
|
||||
"**/*.js"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { Preview } from '@storybook/react'
|
||||
import { withThemeByDataAttribute } from '@storybook/addon-themes'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import I18N from '../app/components/i18n'
|
||||
import { ToastProvider } from '../app/components/base/toast'
|
||||
import I18N from '../app/components/i18n'
|
||||
|
||||
import '../app/styles/globals.css'
|
||||
import '../app/styles/markdown.scss'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import { useState } from 'react'
|
||||
import { useAppForm } from '@/app/components/base/form'
|
||||
|
||||
type UseAppFormOptions = Parameters<typeof useAppForm>[0]
|
||||
|
|
@ -49,7 +49,12 @@ export const FormStoryWrapper = ({
|
|||
<aside className="w-full max-w-sm rounded-xl border border-divider-subtle bg-components-panel-bg p-4 text-xs text-text-secondary shadow-sm">
|
||||
<div className="flex items-center justify-between text-[11px] uppercase tracking-wide text-text-tertiary">
|
||||
<span>Form State</span>
|
||||
<span>{submitCount} submit{submitCount === 1 ? '' : 's'}</span>
|
||||
<span>
|
||||
{submitCount}
|
||||
{' '}
|
||||
submit
|
||||
{submitCount === 1 ? '' : 's'}
|
||||
</span>
|
||||
</div>
|
||||
<dl className="mt-2 space-y-1">
|
||||
<div className="flex items-center justify-between rounded-md bg-components-button-tertiary-bg px-2 py-1">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"firsttris.vscode-jest-runner",
|
||||
"kisstkondoros.vscode-codemetrics"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,50 @@
|
|||
{
|
||||
// Disable the default formatter, use eslint instead
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"eslint.format.enable": true,
|
||||
"[python]": {
|
||||
"editor.formatOnType": true
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"npm.packageManager": "pnpm"
|
||||
|
||||
// Silent the stylistic rules in your IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||
],
|
||||
|
||||
// Enable eslint for all supported languages
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml",
|
||||
"xml",
|
||||
"gql",
|
||||
"graphql",
|
||||
"astro",
|
||||
"svelte",
|
||||
"css",
|
||||
"less",
|
||||
"scss",
|
||||
"pcss",
|
||||
"postcss"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,14 +99,14 @@ If your IDE is VSCode, rename `web/.vscode/settings.example.json` to `web/.vscod
|
|||
|
||||
## Test
|
||||
|
||||
We use [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) for Unit Testing.
|
||||
We use [Vitest](https://vitest.dev/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) for Unit Testing.
|
||||
|
||||
**📖 Complete Testing Guide**: See [web/testing/testing.md](./testing/testing.md) for detailed testing specifications, best practices, and examples.
|
||||
|
||||
Run test:
|
||||
|
||||
```bash
|
||||
pnpm run test
|
||||
pnpm test
|
||||
```
|
||||
|
||||
### Example Code
|
||||
|
|
|
|||
|
|
@ -1,8 +1,40 @@
|
|||
import { merge, noop } from 'lodash-es'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
import { baseProviderContextValue } from '@/context/provider-context'
|
||||
import type { ProviderContextState } from '@/context/provider-context'
|
||||
import type { Plan, UsagePlanInfo } from '@/app/components/billing/type'
|
||||
import type { ProviderContextState } from '@/context/provider-context'
|
||||
import { merge, noop } from 'es-toolkit/compat'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
|
||||
// Avoid being mocked in tests
|
||||
export const baseProviderContextValue: ProviderContextState = {
|
||||
modelProviders: [],
|
||||
refreshModelProviders: noop,
|
||||
textGenerationModelList: [],
|
||||
supportRetrievalMethods: [],
|
||||
isAPIKeySet: true,
|
||||
plan: defaultPlan,
|
||||
isFetchedPlan: false,
|
||||
enableBilling: false,
|
||||
onPlanInfoChanged: noop,
|
||||
enableReplaceWebAppLogo: false,
|
||||
modelLoadBalancingEnabled: false,
|
||||
datasetOperatorEnabled: false,
|
||||
enableEducationPlan: false,
|
||||
isEducationWorkspace: false,
|
||||
isEducationAccount: false,
|
||||
allowRefreshEducationVerify: false,
|
||||
educationAccountExpireAt: null,
|
||||
isLoadingEducationAccountInfo: false,
|
||||
isFetchingEducationAccountInfo: false,
|
||||
webappCopyrightEnabled: false,
|
||||
licenseLimit: {
|
||||
workspace_members: {
|
||||
size: 0,
|
||||
limit: 0,
|
||||
},
|
||||
},
|
||||
refreshLicenseLimit: noop,
|
||||
isAllowTransferWorkspace: false,
|
||||
isAllowPublishAsCustomKnowledgePipelineTemplate: false,
|
||||
}
|
||||
|
||||
export const createMockProviderContextValue = (overrides: Partial<ProviderContextState> = {}): ProviderContextState => {
|
||||
const merged = merge({}, baseProviderContextValue, overrides)
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
/**
|
||||
* Shared mock for react-i18next
|
||||
*
|
||||
* Jest automatically uses this mock when react-i18next is imported in tests.
|
||||
* The default behavior returns the translation key as-is, which is suitable
|
||||
* for most test scenarios.
|
||||
*
|
||||
* For tests that need custom translations, you can override with jest.mock():
|
||||
*
|
||||
* @example
|
||||
* jest.mock('react-i18next', () => ({
|
||||
* useTranslation: () => ({
|
||||
* t: (key: string) => {
|
||||
* if (key === 'some.key') return 'Custom translation'
|
||||
* return key
|
||||
* },
|
||||
* }),
|
||||
* }))
|
||||
*/
|
||||
|
||||
export const useTranslation = () => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
if (options?.returnObjects)
|
||||
return [`${key}-feature-1`, `${key}-feature-2`]
|
||||
if (options)
|
||||
return `${key}:${JSON.stringify(options)}`
|
||||
return key
|
||||
},
|
||||
i18n: {
|
||||
language: 'en',
|
||||
changeLanguage: jest.fn(),
|
||||
},
|
||||
})
|
||||
|
||||
export const Trans = ({ children }: { children?: React.ReactNode }) => children
|
||||
|
||||
export const initReactI18next = {
|
||||
type: '3rdParty',
|
||||
init: jest.fn(),
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import vm from 'node:vm'
|
||||
import { transpile } from 'typescript'
|
||||
|
||||
// Mock functions to simulate the check-i18n functionality
|
||||
const vm = require('node:vm')
|
||||
const transpile = require('typescript').transpile
|
||||
|
||||
describe('check-i18n script functionality', () => {
|
||||
describe('i18n:check script functionality', () => {
|
||||
const testDir = path.join(__dirname, '../i18n-test')
|
||||
const testEnDir = path.join(testDir, 'en-US')
|
||||
const testZhDir = path.join(testDir, 'zh-Hans')
|
||||
|
|
@ -33,8 +31,7 @@ describe('check-i18n script functionality', () => {
|
|||
const filePath = path.join(folderPath, file)
|
||||
const fileName = file.replace(/\.[^/.]+$/, '')
|
||||
const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) =>
|
||||
c.toUpperCase(),
|
||||
)
|
||||
c.toUpperCase())
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8')
|
||||
|
|
@ -617,9 +614,10 @@ export default translation
|
|||
|
||||
// Check if this line ends the value (ends with quote and comma/no comma)
|
||||
if ((trimmed.endsWith('\',') || trimmed.endsWith('",') || trimmed.endsWith('`,')
|
||||
|| trimmed.endsWith('\'') || trimmed.endsWith('"') || trimmed.endsWith('`'))
|
||||
&& !trimmed.startsWith('//'))
|
||||
|| trimmed.endsWith('\'') || trimmed.endsWith('"') || trimmed.endsWith('`'))
|
||||
&& !trimmed.startsWith('//')) {
|
||||
break
|
||||
}
|
||||
}
|
||||
else {
|
||||
break
|
||||
|
|
|
|||
|
|
@ -15,19 +15,19 @@ describe('Description Validation Logic', () => {
|
|||
}
|
||||
|
||||
describe('Backend Validation Function', () => {
|
||||
test('allows description within 400 characters', () => {
|
||||
it('allows description within 400 characters', () => {
|
||||
const validDescription = 'x'.repeat(400)
|
||||
expect(() => validateDescriptionLength(validDescription)).not.toThrow()
|
||||
expect(validateDescriptionLength(validDescription)).toBe(validDescription)
|
||||
})
|
||||
|
||||
test('allows empty description', () => {
|
||||
it('allows empty description', () => {
|
||||
expect(() => validateDescriptionLength('')).not.toThrow()
|
||||
expect(() => validateDescriptionLength(null)).not.toThrow()
|
||||
expect(() => validateDescriptionLength(undefined)).not.toThrow()
|
||||
})
|
||||
|
||||
test('rejects description exceeding 400 characters', () => {
|
||||
it('rejects description exceeding 400 characters', () => {
|
||||
const invalidDescription = 'x'.repeat(401)
|
||||
expect(() => validateDescriptionLength(invalidDescription)).toThrow(
|
||||
'Description cannot exceed 400 characters.',
|
||||
|
|
@ -36,7 +36,7 @@ describe('Description Validation Logic', () => {
|
|||
})
|
||||
|
||||
describe('Backend Validation Consistency', () => {
|
||||
test('App and Dataset have consistent validation limits', () => {
|
||||
it('App and Dataset have consistent validation limits', () => {
|
||||
const maxLength = 400
|
||||
const validDescription = 'x'.repeat(maxLength)
|
||||
const invalidDescription = 'x'.repeat(maxLength + 1)
|
||||
|
|
@ -50,7 +50,7 @@ describe('Description Validation Logic', () => {
|
|||
expect(() => validateDescriptionLength(invalidDescription)).toThrow()
|
||||
})
|
||||
|
||||
test('validation error messages are consistent', () => {
|
||||
it('validation error messages are consistent', () => {
|
||||
const expectedErrorMessage = 'Description cannot exceed 400 characters.'
|
||||
|
||||
// This would be the error message from both App and Dataset backend validation
|
||||
|
|
@ -78,7 +78,7 @@ describe('Description Validation Logic', () => {
|
|||
]
|
||||
|
||||
testCases.forEach(({ length, shouldPass, description }) => {
|
||||
test(`handles ${description} correctly`, () => {
|
||||
it(`handles ${description} correctly`, () => {
|
||||
const testDescription = length > 0 ? 'x'.repeat(length) : ''
|
||||
expect(testDescription.length).toBe(length)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { Mock } from 'vitest'
|
||||
/**
|
||||
* Document Detail Navigation Fix Verification Test
|
||||
*
|
||||
|
|
@ -10,36 +11,36 @@ import { useRouter } from 'next/navigation'
|
|||
import { useDocumentDetail, useDocumentMetadata } from '@/service/knowledge/use-document'
|
||||
|
||||
// Mock Next.js router
|
||||
const mockPush = jest.fn()
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: jest.fn(() => ({
|
||||
const mockPush = vi.fn()
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: vi.fn(() => ({
|
||||
push: mockPush,
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock the document service hooks
|
||||
jest.mock('@/service/knowledge/use-document', () => ({
|
||||
useDocumentDetail: jest.fn(),
|
||||
useDocumentMetadata: jest.fn(),
|
||||
useInvalidDocumentList: jest.fn(() => jest.fn()),
|
||||
vi.mock('@/service/knowledge/use-document', () => ({
|
||||
useDocumentDetail: vi.fn(),
|
||||
useDocumentMetadata: vi.fn(),
|
||||
useInvalidDocumentList: vi.fn(() => vi.fn()),
|
||||
}))
|
||||
|
||||
// Mock other dependencies
|
||||
jest.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContext: jest.fn(() => [null]),
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContext: vi.fn(() => [null]),
|
||||
}))
|
||||
|
||||
jest.mock('@/service/use-base', () => ({
|
||||
useInvalid: jest.fn(() => jest.fn()),
|
||||
vi.mock('@/service/use-base', () => ({
|
||||
useInvalid: vi.fn(() => vi.fn()),
|
||||
}))
|
||||
|
||||
jest.mock('@/service/knowledge/use-segment', () => ({
|
||||
useSegmentListKey: jest.fn(),
|
||||
useChildSegmentListKey: jest.fn(),
|
||||
vi.mock('@/service/knowledge/use-segment', () => ({
|
||||
useSegmentListKey: vi.fn(),
|
||||
useChildSegmentListKey: vi.fn(),
|
||||
}))
|
||||
|
||||
// Create a minimal version of the DocumentDetail component that includes our fix
|
||||
const DocumentDetailWithFix = ({ datasetId, documentId }: { datasetId: string; documentId: string }) => {
|
||||
const DocumentDetailWithFix = ({ datasetId, documentId }: { datasetId: string, documentId: string }) => {
|
||||
const router = useRouter()
|
||||
|
||||
// This is the FIXED implementation from detail/index.tsx
|
||||
|
|
@ -58,7 +59,12 @@ const DocumentDetailWithFix = ({ datasetId, documentId }: { datasetId: string; d
|
|||
Back to Documents
|
||||
</button>
|
||||
<div data-testid="document-info">
|
||||
Dataset: {datasetId}, Document: {documentId}
|
||||
Dataset:
|
||||
{' '}
|
||||
{datasetId}
|
||||
, Document:
|
||||
{' '}
|
||||
{documentId}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -66,10 +72,10 @@ const DocumentDetailWithFix = ({ datasetId, documentId }: { datasetId: string; d
|
|||
|
||||
describe('Document Detail Navigation Fix Verification', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock successful API responses
|
||||
;(useDocumentDetail as jest.Mock).mockReturnValue({
|
||||
;(useDocumentDetail as Mock).mockReturnValue({
|
||||
data: {
|
||||
id: 'doc-123',
|
||||
name: 'Test Document',
|
||||
|
|
@ -80,14 +86,14 @@ describe('Document Detail Navigation Fix Verification', () => {
|
|||
error: null,
|
||||
})
|
||||
|
||||
;(useDocumentMetadata as jest.Mock).mockReturnValue({
|
||||
;(useDocumentMetadata as Mock).mockReturnValue({
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query Parameter Preservation', () => {
|
||||
test('preserves pagination state (page 3, limit 25)', () => {
|
||||
it('preserves pagination state (page 3, limit 25)', () => {
|
||||
// Simulate user coming from page 3 with 25 items per page
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
|
|
@ -107,7 +113,7 @@ describe('Document Detail Navigation Fix Verification', () => {
|
|||
console.log('✅ Pagination state preserved: page=3&limit=25')
|
||||
})
|
||||
|
||||
test('preserves search keyword and filters', () => {
|
||||
it('preserves search keyword and filters', () => {
|
||||
// Simulate user with search and filters applied
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
|
|
@ -126,7 +132,7 @@ describe('Document Detail Navigation Fix Verification', () => {
|
|||
console.log('✅ Search and filters preserved')
|
||||
})
|
||||
|
||||
test('handles complex query parameters with special characters', () => {
|
||||
it('handles complex query parameters with special characters', () => {
|
||||
// Test with complex query string including encoded characters
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
|
|
@ -151,7 +157,7 @@ describe('Document Detail Navigation Fix Verification', () => {
|
|||
console.log('✅ Complex query parameters handled:', expectedCall)
|
||||
})
|
||||
|
||||
test('handles empty query parameters gracefully', () => {
|
||||
it('handles empty query parameters gracefully', () => {
|
||||
// No query parameters in URL
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
|
|
@ -172,7 +178,7 @@ describe('Document Detail Navigation Fix Verification', () => {
|
|||
})
|
||||
|
||||
describe('Different Dataset IDs', () => {
|
||||
test('works with different dataset identifiers', () => {
|
||||
it('works with different dataset identifiers', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
search: '?page=5&limit=10',
|
||||
|
|
@ -192,7 +198,7 @@ describe('Document Detail Navigation Fix Verification', () => {
|
|||
})
|
||||
|
||||
describe('Real User Scenarios', () => {
|
||||
test('scenario: user searches, goes to page 3, views document, clicks back', () => {
|
||||
it('scenario: user searches, goes to page 3, views document, clicks back', () => {
|
||||
// User searched for "API" and navigated to page 3
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
|
|
@ -212,7 +218,7 @@ describe('Document Detail Navigation Fix Verification', () => {
|
|||
console.log('✅ Real user scenario: search + pagination preserved')
|
||||
})
|
||||
|
||||
test('scenario: user applies multiple filters, goes to document, returns', () => {
|
||||
it('scenario: user applies multiple filters, goes to document, returns', () => {
|
||||
// User has applied multiple filters and is on page 2
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
|
|
@ -233,7 +239,7 @@ describe('Document Detail Navigation Fix Verification', () => {
|
|||
})
|
||||
|
||||
describe('Error Handling and Edge Cases', () => {
|
||||
test('handles malformed query parameters gracefully', () => {
|
||||
it('handles malformed query parameters gracefully', () => {
|
||||
// Test with potentially problematic query string
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
|
|
@ -257,7 +263,7 @@ describe('Document Detail Navigation Fix Verification', () => {
|
|||
console.log('✅ Malformed parameters handled gracefully:', navigationPath)
|
||||
})
|
||||
|
||||
test('handles very long query strings', () => {
|
||||
it('handles very long query strings', () => {
|
||||
// Test with a very long query string
|
||||
const longKeyword = 'a'.repeat(1000)
|
||||
Object.defineProperty(window, 'location', {
|
||||
|
|
@ -280,7 +286,7 @@ describe('Document Detail Navigation Fix Verification', () => {
|
|||
})
|
||||
|
||||
describe('Performance Verification', () => {
|
||||
test('navigation function executes quickly', () => {
|
||||
it('navigation function executes quickly', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
search: '?page=1&limit=10&keyword=test',
|
||||
|
|
|
|||
|
|
@ -46,32 +46,32 @@ describe('Document List Sorting', () => {
|
|||
})
|
||||
}
|
||||
|
||||
test('sorts by name descending (default for UI consistency)', () => {
|
||||
it('sorts by name descending (default for UI consistency)', () => {
|
||||
const sorted = sortDocuments(mockDocuments, 'name', 'desc')
|
||||
expect(sorted.map(doc => doc.name)).toEqual(['Gamma.docx', 'Beta.pdf', 'Alpha.txt'])
|
||||
})
|
||||
|
||||
test('sorts by name ascending (after toggle)', () => {
|
||||
it('sorts by name ascending (after toggle)', () => {
|
||||
const sorted = sortDocuments(mockDocuments, 'name', 'asc')
|
||||
expect(sorted.map(doc => doc.name)).toEqual(['Alpha.txt', 'Beta.pdf', 'Gamma.docx'])
|
||||
})
|
||||
|
||||
test('sorts by word_count descending', () => {
|
||||
it('sorts by word_count descending', () => {
|
||||
const sorted = sortDocuments(mockDocuments, 'word_count', 'desc')
|
||||
expect(sorted.map(doc => doc.word_count)).toEqual([800, 500, 200])
|
||||
})
|
||||
|
||||
test('sorts by hit_count descending', () => {
|
||||
it('sorts by hit_count descending', () => {
|
||||
const sorted = sortDocuments(mockDocuments, 'hit_count', 'desc')
|
||||
expect(sorted.map(doc => doc.hit_count)).toEqual([25, 10, 5])
|
||||
})
|
||||
|
||||
test('sorts by created_at descending (newest first)', () => {
|
||||
it('sorts by created_at descending (newest first)', () => {
|
||||
const sorted = sortDocuments(mockDocuments, 'created_at', 'desc')
|
||||
expect(sorted.map(doc => doc.created_at)).toEqual([1699123500, 1699123456, 1699123400])
|
||||
})
|
||||
|
||||
test('handles empty values correctly', () => {
|
||||
it('handles empty values correctly', () => {
|
||||
const docsWithEmpty = [
|
||||
{ id: '1', name: 'Test', word_count: 100, hit_count: 5, created_at: 1699123456 },
|
||||
{ id: '2', name: 'Empty', word_count: 0, hit_count: 0, created_at: 1699123400 },
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import React from 'react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
|
||||
import MailAndPasswordAuth from '@/app/(shareLayout)/webapp-signin/components/mail-and-password-auth'
|
||||
import CheckCode from '@/app/(shareLayout)/webapp-signin/check-code/page'
|
||||
import MailAndPasswordAuth from '@/app/(shareLayout)/webapp-signin/components/mail-and-password-auth'
|
||||
|
||||
const replaceMock = jest.fn()
|
||||
const backMock = jest.fn()
|
||||
const replaceMock = vi.fn()
|
||||
const backMock = vi.fn()
|
||||
const useSearchParamsMock = vi.fn(() => new URLSearchParams())
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
usePathname: jest.fn(() => '/chatbot/test-app'),
|
||||
useRouter: jest.fn(() => ({
|
||||
vi.mock('next/navigation', () => ({
|
||||
usePathname: vi.fn(() => '/chatbot/test-app'),
|
||||
useRouter: vi.fn(() => ({
|
||||
replace: replaceMock,
|
||||
back: backMock,
|
||||
})),
|
||||
useSearchParams: jest.fn(),
|
||||
useSearchParams: () => useSearchParamsMock(),
|
||||
}))
|
||||
|
||||
const mockStoreState = {
|
||||
|
|
@ -21,59 +22,55 @@ const mockStoreState = {
|
|||
shareCode: 'test-app',
|
||||
}
|
||||
|
||||
const useWebAppStoreMock = jest.fn((selector?: (state: typeof mockStoreState) => any) => {
|
||||
const useWebAppStoreMock = vi.fn((selector?: (state: typeof mockStoreState) => any) => {
|
||||
return selector ? selector(mockStoreState) : mockStoreState
|
||||
})
|
||||
|
||||
jest.mock('@/context/web-app-context', () => ({
|
||||
vi.mock('@/context/web-app-context', () => ({
|
||||
useWebAppStore: (selector?: (state: typeof mockStoreState) => any) => useWebAppStoreMock(selector),
|
||||
}))
|
||||
|
||||
const webAppLoginMock = jest.fn()
|
||||
const webAppEmailLoginWithCodeMock = jest.fn()
|
||||
const sendWebAppEMailLoginCodeMock = jest.fn()
|
||||
const webAppLoginMock = vi.fn()
|
||||
const webAppEmailLoginWithCodeMock = vi.fn()
|
||||
const sendWebAppEMailLoginCodeMock = vi.fn()
|
||||
|
||||
jest.mock('@/service/common', () => ({
|
||||
vi.mock('@/service/common', () => ({
|
||||
webAppLogin: (...args: any[]) => webAppLoginMock(...args),
|
||||
webAppEmailLoginWithCode: (...args: any[]) => webAppEmailLoginWithCodeMock(...args),
|
||||
sendWebAppEMailLoginCode: (...args: any[]) => sendWebAppEMailLoginCodeMock(...args),
|
||||
}))
|
||||
|
||||
const fetchAccessTokenMock = jest.fn()
|
||||
const fetchAccessTokenMock = vi.fn()
|
||||
|
||||
jest.mock('@/service/share', () => ({
|
||||
vi.mock('@/service/share', () => ({
|
||||
fetchAccessToken: (...args: any[]) => fetchAccessTokenMock(...args),
|
||||
}))
|
||||
|
||||
const setWebAppAccessTokenMock = jest.fn()
|
||||
const setWebAppPassportMock = jest.fn()
|
||||
const setWebAppAccessTokenMock = vi.fn()
|
||||
const setWebAppPassportMock = vi.fn()
|
||||
|
||||
jest.mock('@/service/webapp-auth', () => ({
|
||||
vi.mock('@/service/webapp-auth', () => ({
|
||||
setWebAppAccessToken: (...args: any[]) => setWebAppAccessTokenMock(...args),
|
||||
setWebAppPassport: (...args: any[]) => setWebAppPassportMock(...args),
|
||||
webAppLogout: jest.fn(),
|
||||
webAppLogout: vi.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/signin/countdown', () => () => <div data-testid="countdown" />)
|
||||
vi.mock('@/app/components/signin/countdown', () => ({ default: () => <div data-testid="countdown" /> }))
|
||||
|
||||
jest.mock('@remixicon/react', () => ({
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiMailSendFill: () => <div data-testid="mail-icon" />,
|
||||
RiArrowLeftLine: () => <div data-testid="arrow-icon" />,
|
||||
}))
|
||||
|
||||
const { useSearchParams } = jest.requireMock('next/navigation') as {
|
||||
useSearchParams: jest.Mock
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('embedded user id propagation in authentication flows', () => {
|
||||
it('passes embedded user id when logging in with email and password', async () => {
|
||||
const params = new URLSearchParams()
|
||||
params.set('redirect_url', encodeURIComponent('/chatbot/test-app'))
|
||||
useSearchParams.mockReturnValue(params)
|
||||
useSearchParamsMock.mockReturnValue(params)
|
||||
|
||||
webAppLoginMock.mockResolvedValue({ result: 'success', data: { access_token: 'login-token' } })
|
||||
fetchAccessTokenMock.mockResolvedValue({ access_token: 'passport-token' })
|
||||
|
|
@ -100,7 +97,7 @@ describe('embedded user id propagation in authentication flows', () => {
|
|||
params.set('redirect_url', encodeURIComponent('/chatbot/test-app'))
|
||||
params.set('email', encodeURIComponent('user@example.com'))
|
||||
params.set('token', encodeURIComponent('token-abc'))
|
||||
useSearchParams.mockReturnValue(params)
|
||||
useSearchParamsMock.mockReturnValue(params)
|
||||
|
||||
webAppEmailLoginWithCodeMock.mockResolvedValue({ result: 'success', data: { access_token: 'code-token' } })
|
||||
fetchAccessTokenMock.mockResolvedValue({ access_token: 'passport-token' })
|
||||
|
|
|
|||
|
|
@ -1,42 +1,42 @@
|
|||
import React from 'react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
|
||||
import * as React from 'react'
|
||||
import WebAppStoreProvider, { useWebAppStore } from '@/context/web-app-context'
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
usePathname: jest.fn(() => '/chatbot/sample-app'),
|
||||
useSearchParams: jest.fn(() => {
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
usePathname: vi.fn(() => '/chatbot/sample-app'),
|
||||
useSearchParams: vi.fn(() => {
|
||||
const params = new URLSearchParams()
|
||||
return params
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock('@/service/use-share', () => {
|
||||
const { AccessMode } = jest.requireActual('@/models/access-control')
|
||||
return {
|
||||
useGetWebAppAccessModeByCode: jest.fn(() => ({
|
||||
isLoading: false,
|
||||
data: { accessMode: AccessMode.PUBLIC },
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/app/components/base/chat/utils', () => ({
|
||||
getProcessedSystemVariablesFromUrlParams: jest.fn(),
|
||||
vi.mock('@/service/use-share', () => ({
|
||||
useGetWebAppAccessModeByCode: vi.fn(() => ({
|
||||
isLoading: false,
|
||||
data: { accessMode: AccessMode.PUBLIC },
|
||||
})),
|
||||
}))
|
||||
|
||||
const { getProcessedSystemVariablesFromUrlParams: mockGetProcessedSystemVariablesFromUrlParams }
|
||||
= jest.requireMock('@/app/components/base/chat/utils') as {
|
||||
getProcessedSystemVariablesFromUrlParams: jest.Mock
|
||||
}
|
||||
// Store the mock implementation in a way that survives hoisting
|
||||
const mockGetProcessedSystemVariablesFromUrlParams = vi.fn()
|
||||
|
||||
jest.mock('@/context/global-public-context', () => {
|
||||
const mockGlobalStoreState = {
|
||||
vi.mock('@/app/components/base/chat/utils', () => ({
|
||||
getProcessedSystemVariablesFromUrlParams: (...args: any[]) => mockGetProcessedSystemVariablesFromUrlParams(...args),
|
||||
}))
|
||||
|
||||
// Use vi.hoisted to define mock state before vi.mock hoisting
|
||||
const { mockGlobalStoreState } = vi.hoisted(() => ({
|
||||
mockGlobalStoreState: {
|
||||
isGlobalPending: false,
|
||||
setIsGlobalPending: jest.fn(),
|
||||
setIsGlobalPending: vi.fn(),
|
||||
systemFeatures: {},
|
||||
setSystemFeatures: jest.fn(),
|
||||
}
|
||||
setSystemFeatures: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/context/global-public-context', () => {
|
||||
const useGlobalPublicStore = Object.assign(
|
||||
(selector?: (state: typeof mockGlobalStoreState) => any) =>
|
||||
selector ? selector(mockGlobalStoreState) : mockGlobalStoreState,
|
||||
|
|
@ -56,21 +56,6 @@ jest.mock('@/context/global-public-context', () => {
|
|||
}
|
||||
})
|
||||
|
||||
const {
|
||||
useGlobalPublicStore: useGlobalPublicStoreMock,
|
||||
} = jest.requireMock('@/context/global-public-context') as {
|
||||
useGlobalPublicStore: ((selector?: (state: any) => any) => any) & {
|
||||
setState: (updater: any) => void
|
||||
__mockState: {
|
||||
isGlobalPending: boolean
|
||||
setIsGlobalPending: jest.Mock
|
||||
systemFeatures: Record<string, unknown>
|
||||
setSystemFeatures: jest.Mock
|
||||
}
|
||||
}
|
||||
}
|
||||
const mockGlobalStoreState = useGlobalPublicStoreMock.__mockState
|
||||
|
||||
const TestConsumer = () => {
|
||||
const embeddedUserId = useWebAppStore(state => state.embeddedUserId)
|
||||
const embeddedConversationId = useWebAppStore(state => state.embeddedConversationId)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import CommandSelector from '../../app/components/goto-anything/command-selector'
|
||||
import type { ActionItem } from '../../app/components/goto-anything/actions/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import CommandSelector from '../../app/components/goto-anything/command-selector'
|
||||
|
||||
jest.mock('cmdk', () => ({
|
||||
vi.mock('cmdk', () => ({
|
||||
Command: {
|
||||
Group: ({ children, className }: any) => <div className={className}>{children}</div>,
|
||||
Item: ({ children, onSelect, value, className }: any) => (
|
||||
|
|
@ -27,36 +26,36 @@ describe('CommandSelector', () => {
|
|||
shortcut: '@app',
|
||||
title: 'Search Applications',
|
||||
description: 'Search apps',
|
||||
search: jest.fn(),
|
||||
search: vi.fn(),
|
||||
},
|
||||
knowledge: {
|
||||
key: '@knowledge',
|
||||
shortcut: '@kb',
|
||||
title: 'Search Knowledge',
|
||||
description: 'Search knowledge bases',
|
||||
search: jest.fn(),
|
||||
search: vi.fn(),
|
||||
},
|
||||
plugin: {
|
||||
key: '@plugin',
|
||||
shortcut: '@plugin',
|
||||
title: 'Search Plugins',
|
||||
description: 'Search plugins',
|
||||
search: jest.fn(),
|
||||
search: vi.fn(),
|
||||
},
|
||||
node: {
|
||||
key: '@node',
|
||||
shortcut: '@node',
|
||||
title: 'Search Nodes',
|
||||
description: 'Search workflow nodes',
|
||||
search: jest.fn(),
|
||||
search: vi.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
const mockOnCommandSelect = jest.fn()
|
||||
const mockOnCommandValueChange = jest.fn()
|
||||
const mockOnCommandSelect = vi.fn()
|
||||
const mockOnCommandValueChange = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Basic Rendering', () => {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import type { Mock } from 'vitest'
|
||||
import type { ActionItem } from '../../app/components/goto-anything/actions/types'
|
||||
|
||||
// Mock the entire actions module to avoid import issues
|
||||
jest.mock('../../app/components/goto-anything/actions', () => ({
|
||||
matchAction: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('../../app/components/goto-anything/actions/commands/registry')
|
||||
|
||||
// Import after mocking to get mocked version
|
||||
import { matchAction } from '../../app/components/goto-anything/actions'
|
||||
import { slashCommandRegistry } from '../../app/components/goto-anything/actions/commands/registry'
|
||||
|
||||
// Mock the entire actions module to avoid import issues
|
||||
vi.mock('../../app/components/goto-anything/actions', () => ({
|
||||
matchAction: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../../app/components/goto-anything/actions/commands/registry')
|
||||
|
||||
// Implement the actual matchAction logic for testing
|
||||
const actualMatchAction = (query: string, actions: Record<string, ActionItem>) => {
|
||||
const result = Object.values(actions).find((action) => {
|
||||
|
|
@ -39,7 +40,7 @@ const actualMatchAction = (query: string, actions: Record<string, ActionItem>) =
|
|||
}
|
||||
|
||||
// Replace mock with actual implementation
|
||||
;(matchAction as jest.Mock).mockImplementation(actualMatchAction)
|
||||
;(matchAction as Mock).mockImplementation(actualMatchAction)
|
||||
|
||||
describe('matchAction Logic', () => {
|
||||
const mockActions: Record<string, ActionItem> = {
|
||||
|
|
@ -48,27 +49,27 @@ describe('matchAction Logic', () => {
|
|||
shortcut: '@a',
|
||||
title: 'Search Applications',
|
||||
description: 'Search apps',
|
||||
search: jest.fn(),
|
||||
search: vi.fn(),
|
||||
},
|
||||
knowledge: {
|
||||
key: '@knowledge',
|
||||
shortcut: '@kb',
|
||||
title: 'Search Knowledge',
|
||||
description: 'Search knowledge bases',
|
||||
search: jest.fn(),
|
||||
search: vi.fn(),
|
||||
},
|
||||
slash: {
|
||||
key: '/',
|
||||
shortcut: '/',
|
||||
title: 'Commands',
|
||||
description: 'Execute commands',
|
||||
search: jest.fn(),
|
||||
search: vi.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(slashCommandRegistry.getAllCommands as jest.Mock).mockReturnValue([
|
||||
vi.clearAllMocks()
|
||||
;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([
|
||||
{ name: 'docs', mode: 'direct' },
|
||||
{ name: 'community', mode: 'direct' },
|
||||
{ name: 'feedback', mode: 'direct' },
|
||||
|
|
@ -188,7 +189,7 @@ describe('matchAction Logic', () => {
|
|||
|
||||
describe('Mode-based Filtering', () => {
|
||||
it('should filter direct mode commands from matching', () => {
|
||||
;(slashCommandRegistry.getAllCommands as jest.Mock).mockReturnValue([
|
||||
;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([
|
||||
{ name: 'test', mode: 'direct' },
|
||||
])
|
||||
|
||||
|
|
@ -197,7 +198,7 @@ describe('matchAction Logic', () => {
|
|||
})
|
||||
|
||||
it('should allow submenu mode commands to match', () => {
|
||||
;(slashCommandRegistry.getAllCommands as jest.Mock).mockReturnValue([
|
||||
;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([
|
||||
{ name: 'test', mode: 'submenu' },
|
||||
])
|
||||
|
||||
|
|
@ -206,7 +207,7 @@ describe('matchAction Logic', () => {
|
|||
})
|
||||
|
||||
it('should treat undefined mode as submenu', () => {
|
||||
;(slashCommandRegistry.getAllCommands as jest.Mock).mockReturnValue([
|
||||
;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([
|
||||
{ name: 'test' }, // No mode specified
|
||||
])
|
||||
|
||||
|
|
@ -227,7 +228,7 @@ describe('matchAction Logic', () => {
|
|||
})
|
||||
|
||||
it('should handle empty command list', () => {
|
||||
;(slashCommandRegistry.getAllCommands as jest.Mock).mockReturnValue([])
|
||||
;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([])
|
||||
const result = matchAction('/anything', mockActions)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import * as React from 'react'
|
||||
|
||||
// Type alias for search mode
|
||||
type SearchMode = 'scopes' | 'commands' | null
|
||||
|
||||
// Mock component to test tag display logic
|
||||
const TagDisplay: React.FC<{ searchMode: SearchMode }> = ({ searchMode }) => {
|
||||
if (!searchMode) return null
|
||||
if (!searchMode)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-xs text-text-tertiary">
|
||||
|
|
@ -38,8 +38,10 @@ describe('Scope and Command Tags', () => {
|
|||
|
||||
describe('Search Mode Detection', () => {
|
||||
const getSearchMode = (query: string): SearchMode => {
|
||||
if (query.startsWith('@')) return 'scopes'
|
||||
if (query.startsWith('/')) return 'commands'
|
||||
if (query.startsWith('@'))
|
||||
return 'scopes'
|
||||
if (query.startsWith('/'))
|
||||
return 'commands'
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -91,8 +93,10 @@ describe('Scope and Command Tags', () => {
|
|||
const SearchComponent: React.FC<{ query: string }> = ({ query }) => {
|
||||
let searchMode: SearchMode = null
|
||||
|
||||
if (query.startsWith('@')) searchMode = 'scopes'
|
||||
else if (query.startsWith('/')) searchMode = 'commands'
|
||||
if (query.startsWith('@'))
|
||||
searchMode = 'scopes'
|
||||
else if (query.startsWith('/'))
|
||||
searchMode = 'commands'
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { MockedFunction } from 'vitest'
|
||||
/**
|
||||
* Test GotoAnything search error handling mechanisms
|
||||
*
|
||||
|
|
@ -9,38 +10,38 @@
|
|||
*/
|
||||
|
||||
import { Actions, searchAnything } from '@/app/components/goto-anything/actions'
|
||||
import { postMarketplace } from '@/service/base'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import { postMarketplace } from '@/service/base'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
|
||||
// Mock API functions
|
||||
jest.mock('@/service/base', () => ({
|
||||
postMarketplace: jest.fn(),
|
||||
vi.mock('@/service/base', () => ({
|
||||
postMarketplace: vi.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/service/apps', () => ({
|
||||
fetchAppList: jest.fn(),
|
||||
vi.mock('@/service/apps', () => ({
|
||||
fetchAppList: vi.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/service/datasets', () => ({
|
||||
fetchDatasets: jest.fn(),
|
||||
vi.mock('@/service/datasets', () => ({
|
||||
fetchDatasets: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockPostMarketplace = postMarketplace as jest.MockedFunction<typeof postMarketplace>
|
||||
const mockFetchAppList = fetchAppList as jest.MockedFunction<typeof fetchAppList>
|
||||
const mockFetchDatasets = fetchDatasets as jest.MockedFunction<typeof fetchDatasets>
|
||||
const mockPostMarketplace = postMarketplace as MockedFunction<typeof postMarketplace>
|
||||
const mockFetchAppList = fetchAppList as MockedFunction<typeof fetchAppList>
|
||||
const mockFetchDatasets = fetchDatasets as MockedFunction<typeof fetchDatasets>
|
||||
|
||||
describe('GotoAnything Search Error Handling', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
vi.clearAllMocks()
|
||||
// Suppress console.warn for clean test output
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {
|
||||
// Suppress console.warn for clean test output
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('@plugin search error handling', () => {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import '@testing-library/jest-dom'
|
||||
import { slashCommandRegistry } from '../../app/components/goto-anything/actions/commands/registry'
|
||||
import type { SlashCommandHandler } from '../../app/components/goto-anything/actions/commands/types'
|
||||
import { slashCommandRegistry } from '../../app/components/goto-anything/actions/commands/registry'
|
||||
|
||||
// Mock the registry
|
||||
jest.mock('../../app/components/goto-anything/actions/commands/registry')
|
||||
vi.mock('../../app/components/goto-anything/actions/commands/registry')
|
||||
|
||||
describe('Slash Command Dual-Mode System', () => {
|
||||
const mockDirectCommand: SlashCommandHandler = {
|
||||
name: 'docs',
|
||||
description: 'Open documentation',
|
||||
mode: 'direct',
|
||||
execute: jest.fn(),
|
||||
search: jest.fn().mockResolvedValue([
|
||||
execute: vi.fn(),
|
||||
search: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'docs',
|
||||
title: 'Documentation',
|
||||
|
|
@ -20,15 +19,15 @@ describe('Slash Command Dual-Mode System', () => {
|
|||
data: { command: 'navigation.docs', args: {} },
|
||||
},
|
||||
]),
|
||||
register: jest.fn(),
|
||||
unregister: jest.fn(),
|
||||
register: vi.fn(),
|
||||
unregister: vi.fn(),
|
||||
}
|
||||
|
||||
const mockSubmenuCommand: SlashCommandHandler = {
|
||||
name: 'theme',
|
||||
description: 'Change theme',
|
||||
mode: 'submenu',
|
||||
search: jest.fn().mockResolvedValue([
|
||||
search: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'theme-light',
|
||||
title: 'Light Theme',
|
||||
|
|
@ -44,18 +43,20 @@ describe('Slash Command Dual-Mode System', () => {
|
|||
data: { command: 'theme.set', args: { theme: 'dark' } },
|
||||
},
|
||||
]),
|
||||
register: jest.fn(),
|
||||
unregister: jest.fn(),
|
||||
register: vi.fn(),
|
||||
unregister: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(slashCommandRegistry as any).findCommand = jest.fn((name: string) => {
|
||||
if (name === 'docs') return mockDirectCommand
|
||||
if (name === 'theme') return mockSubmenuCommand
|
||||
vi.clearAllMocks()
|
||||
;(slashCommandRegistry as any).findCommand = vi.fn((name: string) => {
|
||||
if (name === 'docs')
|
||||
return mockDirectCommand
|
||||
if (name === 'theme')
|
||||
return mockSubmenuCommand
|
||||
return null
|
||||
})
|
||||
;(slashCommandRegistry as any).getAllCommands = jest.fn(() => [
|
||||
;(slashCommandRegistry as any).getAllCommands = vi.fn(() => [
|
||||
mockDirectCommand,
|
||||
mockSubmenuCommand,
|
||||
])
|
||||
|
|
@ -63,8 +64,8 @@ describe('Slash Command Dual-Mode System', () => {
|
|||
|
||||
describe('Direct Mode Commands', () => {
|
||||
it('should execute immediately when selected', () => {
|
||||
const mockSetShow = jest.fn()
|
||||
const mockSetSearchQuery = jest.fn()
|
||||
const mockSetShow = vi.fn()
|
||||
const mockSetSearchQuery = vi.fn()
|
||||
|
||||
// Simulate command selection
|
||||
const handler = slashCommandRegistry.findCommand('docs')
|
||||
|
|
@ -88,7 +89,7 @@ describe('Slash Command Dual-Mode System', () => {
|
|||
})
|
||||
|
||||
it('should close modal after execution', () => {
|
||||
const mockModalClose = jest.fn()
|
||||
const mockModalClose = vi.fn()
|
||||
|
||||
const handler = slashCommandRegistry.findCommand('docs')
|
||||
if (handler?.mode === 'direct' && handler.execute) {
|
||||
|
|
@ -118,7 +119,7 @@ describe('Slash Command Dual-Mode System', () => {
|
|||
})
|
||||
|
||||
it('should keep modal open for selection', () => {
|
||||
const mockModalClose = jest.fn()
|
||||
const mockModalClose = vi.fn()
|
||||
|
||||
const handler = slashCommandRegistry.findCommand('theme')
|
||||
// For submenu mode, modal should not close immediately
|
||||
|
|
@ -141,12 +142,12 @@ describe('Slash Command Dual-Mode System', () => {
|
|||
const commandWithoutMode: SlashCommandHandler = {
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
search: jest.fn(),
|
||||
register: jest.fn(),
|
||||
unregister: jest.fn(),
|
||||
search: vi.fn(),
|
||||
register: vi.fn(),
|
||||
unregister: vi.fn(),
|
||||
}
|
||||
|
||||
;(slashCommandRegistry as any).findCommand = jest.fn(() => commandWithoutMode)
|
||||
;(slashCommandRegistry as any).findCommand = vi.fn(() => commandWithoutMode)
|
||||
|
||||
const handler = slashCommandRegistry.findCommand('test')
|
||||
// Default behavior should be submenu when mode is not specified
|
||||
|
|
@ -189,7 +190,7 @@ describe('Slash Command Dual-Mode System', () => {
|
|||
describe('Command Registration', () => {
|
||||
it('should register both direct and submenu commands', () => {
|
||||
mockDirectCommand.register?.({})
|
||||
mockSubmenuCommand.register?.({ setTheme: jest.fn() })
|
||||
mockSubmenuCommand.register?.({ setTheme: vi.fn() })
|
||||
|
||||
expect(mockDirectCommand.register).toHaveBeenCalled()
|
||||
expect(mockSubmenuCommand.register).toHaveBeenCalled()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const getSupportedLocales = (): string[] => {
|
|||
|
||||
// Helper function to load translation file content
|
||||
const loadTranslationContent = (locale: string): string => {
|
||||
const filePath = path.join(I18N_DIR, locale, 'app-debug.ts')
|
||||
const filePath = path.join(I18N_DIR, locale, 'app-debug.json')
|
||||
|
||||
if (!fs.existsSync(filePath))
|
||||
throw new Error(`Translation file not found: ${filePath}`)
|
||||
|
|
@ -24,14 +24,14 @@ const loadTranslationContent = (locale: string): string => {
|
|||
return fs.readFileSync(filePath, 'utf-8')
|
||||
}
|
||||
|
||||
// Helper function to check if upload features exist
|
||||
// Helper function to check if upload features exist (supports flattened JSON)
|
||||
const hasUploadFeatures = (content: string): { [key: string]: boolean } => {
|
||||
return {
|
||||
fileUpload: /fileUpload\s*:\s*{/.test(content),
|
||||
imageUpload: /imageUpload\s*:\s*{/.test(content),
|
||||
documentUpload: /documentUpload\s*:\s*{/.test(content),
|
||||
audioUpload: /audioUpload\s*:\s*{/.test(content),
|
||||
featureBar: /bar\s*:\s*{/.test(content),
|
||||
fileUpload: /"feature\.fileUpload\.title"/.test(content),
|
||||
imageUpload: /"feature\.imageUpload\.title"/.test(content),
|
||||
documentUpload: /"feature\.documentUpload\.title"/.test(content),
|
||||
audioUpload: /"feature\.audioUpload\.title"/.test(content),
|
||||
featureBar: /"feature\.bar\.empty"/.test(content),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,14 +43,14 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
|
|||
console.log(`Testing ${supportedLocales.length} locales for upload features`)
|
||||
})
|
||||
|
||||
test('all locales should have translation files', () => {
|
||||
it('all locales should have translation files', () => {
|
||||
supportedLocales.forEach((locale) => {
|
||||
const filePath = path.join(I18N_DIR, locale, 'app-debug.ts')
|
||||
const filePath = path.join(I18N_DIR, locale, 'app-debug.json')
|
||||
expect(fs.existsSync(filePath)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test('all locales should have required upload features', () => {
|
||||
it('all locales should have required upload features', () => {
|
||||
const results: { [locale: string]: { [feature: string]: boolean } } = {}
|
||||
|
||||
supportedLocales.forEach((locale) => {
|
||||
|
|
@ -69,50 +69,47 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
|
|||
console.log('✅ All locales have complete upload features')
|
||||
})
|
||||
|
||||
test('previously missing locales should now have audioUpload - Issue #23062', () => {
|
||||
it('previously missing locales should now have audioUpload - Issue #23062', () => {
|
||||
// These locales were specifically missing audioUpload
|
||||
const previouslyMissingLocales = ['fa-IR', 'hi-IN', 'ro-RO', 'sl-SI', 'th-TH', 'uk-UA', 'vi-VN']
|
||||
|
||||
previouslyMissingLocales.forEach((locale) => {
|
||||
const content = loadTranslationContent(locale)
|
||||
|
||||
// Verify audioUpload exists
|
||||
expect(/audioUpload\s*:\s*{/.test(content)).toBe(true)
|
||||
|
||||
// Verify it has title and description
|
||||
expect(/audioUpload[^}]*title\s*:/.test(content)).toBe(true)
|
||||
expect(/audioUpload[^}]*description\s*:/.test(content)).toBe(true)
|
||||
// Verify audioUpload exists with title and description (flattened JSON format)
|
||||
expect(/"feature\.audioUpload\.title"/.test(content)).toBe(true)
|
||||
expect(/"feature\.audioUpload\.description"/.test(content)).toBe(true)
|
||||
|
||||
console.log(`✅ ${locale} - Issue #23062 resolved: audioUpload feature present`)
|
||||
})
|
||||
})
|
||||
|
||||
test('upload features should have required properties', () => {
|
||||
it('upload features should have required properties', () => {
|
||||
supportedLocales.forEach((locale) => {
|
||||
const content = loadTranslationContent(locale)
|
||||
|
||||
// Check fileUpload has required properties
|
||||
if (/fileUpload\s*:\s*{/.test(content)) {
|
||||
expect(/fileUpload[^}]*title\s*:/.test(content)).toBe(true)
|
||||
expect(/fileUpload[^}]*description\s*:/.test(content)).toBe(true)
|
||||
// Check fileUpload has required properties (flattened JSON format)
|
||||
if (/"feature\.fileUpload\.title"/.test(content)) {
|
||||
expect(/"feature\.fileUpload\.title"/.test(content)).toBe(true)
|
||||
expect(/"feature\.fileUpload\.description"/.test(content)).toBe(true)
|
||||
}
|
||||
|
||||
// Check imageUpload has required properties
|
||||
if (/imageUpload\s*:\s*{/.test(content)) {
|
||||
expect(/imageUpload[^}]*title\s*:/.test(content)).toBe(true)
|
||||
expect(/imageUpload[^}]*description\s*:/.test(content)).toBe(true)
|
||||
if (/"feature\.imageUpload\.title"/.test(content)) {
|
||||
expect(/"feature\.imageUpload\.title"/.test(content)).toBe(true)
|
||||
expect(/"feature\.imageUpload\.description"/.test(content)).toBe(true)
|
||||
}
|
||||
|
||||
// Check documentUpload has required properties
|
||||
if (/documentUpload\s*:\s*{/.test(content)) {
|
||||
expect(/documentUpload[^}]*title\s*:/.test(content)).toBe(true)
|
||||
expect(/documentUpload[^}]*description\s*:/.test(content)).toBe(true)
|
||||
if (/"feature\.documentUpload\.title"/.test(content)) {
|
||||
expect(/"feature\.documentUpload\.title"/.test(content)).toBe(true)
|
||||
expect(/"feature\.documentUpload\.description"/.test(content)).toBe(true)
|
||||
}
|
||||
|
||||
// Check audioUpload has required properties
|
||||
if (/audioUpload\s*:\s*{/.test(content)) {
|
||||
expect(/audioUpload[^}]*title\s*:/.test(content)).toBe(true)
|
||||
expect(/audioUpload[^}]*description\s*:/.test(content)).toBe(true)
|
||||
if (/"feature\.audioUpload\.title"/.test(content)) {
|
||||
expect(/"feature\.audioUpload\.title"/.test(content)).toBe(true)
|
||||
expect(/"feature\.audioUpload\.description"/.test(content)).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -15,16 +15,16 @@ import {
|
|||
} from '@/utils/navigation'
|
||||
|
||||
// Mock router for testing
|
||||
const mockPush = jest.fn()
|
||||
const mockPush = vi.fn()
|
||||
const mockRouter = { push: mockPush }
|
||||
|
||||
describe('Navigation Utilities', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('createNavigationPath', () => {
|
||||
test('preserves query parameters by default', () => {
|
||||
it('preserves query parameters by default', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=3&limit=10&keyword=test' },
|
||||
writable: true,
|
||||
|
|
@ -34,7 +34,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(path).toBe('/datasets/123/documents?page=3&limit=10&keyword=test')
|
||||
})
|
||||
|
||||
test('returns clean path when preserveParams is false', () => {
|
||||
it('returns clean path when preserveParams is false', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=3&limit=10' },
|
||||
writable: true,
|
||||
|
|
@ -44,7 +44,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(path).toBe('/datasets/123/documents')
|
||||
})
|
||||
|
||||
test('handles empty query parameters', () => {
|
||||
it('handles empty query parameters', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '' },
|
||||
writable: true,
|
||||
|
|
@ -54,7 +54,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(path).toBe('/datasets/123/documents')
|
||||
})
|
||||
|
||||
test('handles errors gracefully', () => {
|
||||
it('handles errors gracefully', () => {
|
||||
// Mock window.location to throw an error
|
||||
Object.defineProperty(window, 'location', {
|
||||
get: () => {
|
||||
|
|
@ -63,7 +63,7 @@ describe('Navigation Utilities', () => {
|
|||
configurable: true,
|
||||
})
|
||||
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { /* noop */ })
|
||||
const path = createNavigationPath('/datasets/123/documents')
|
||||
|
||||
expect(path).toBe('/datasets/123/documents')
|
||||
|
|
@ -74,7 +74,7 @@ describe('Navigation Utilities', () => {
|
|||
})
|
||||
|
||||
describe('createBackNavigation', () => {
|
||||
test('creates function that navigates with preserved params', () => {
|
||||
it('creates function that navigates with preserved params', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=2&limit=25' },
|
||||
writable: true,
|
||||
|
|
@ -86,7 +86,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(mockPush).toHaveBeenCalledWith('/datasets/123/documents?page=2&limit=25')
|
||||
})
|
||||
|
||||
test('creates function that navigates without params when specified', () => {
|
||||
it('creates function that navigates without params when specified', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=2&limit=25' },
|
||||
writable: true,
|
||||
|
|
@ -100,7 +100,7 @@ describe('Navigation Utilities', () => {
|
|||
})
|
||||
|
||||
describe('extractQueryParams', () => {
|
||||
test('extracts specified parameters', () => {
|
||||
it('extracts specified parameters', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=3&limit=10&keyword=test&other=value' },
|
||||
writable: true,
|
||||
|
|
@ -114,7 +114,7 @@ describe('Navigation Utilities', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('handles missing parameters', () => {
|
||||
it('handles missing parameters', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=3' },
|
||||
writable: true,
|
||||
|
|
@ -126,7 +126,7 @@ describe('Navigation Utilities', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('handles errors gracefully', () => {
|
||||
it('handles errors gracefully', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
get: () => {
|
||||
throw new Error('Location access denied')
|
||||
|
|
@ -134,7 +134,7 @@ describe('Navigation Utilities', () => {
|
|||
configurable: true,
|
||||
})
|
||||
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { /* noop */ })
|
||||
const params = extractQueryParams(['page', 'limit'])
|
||||
|
||||
expect(params).toEqual({})
|
||||
|
|
@ -145,7 +145,7 @@ describe('Navigation Utilities', () => {
|
|||
})
|
||||
|
||||
describe('createNavigationPathWithParams', () => {
|
||||
test('creates path with specified parameters', () => {
|
||||
it('creates path with specified parameters', () => {
|
||||
const path = createNavigationPathWithParams('/datasets/123/documents', {
|
||||
page: 1,
|
||||
limit: 25,
|
||||
|
|
@ -155,7 +155,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(path).toBe('/datasets/123/documents?page=1&limit=25&keyword=search+term')
|
||||
})
|
||||
|
||||
test('filters out empty values', () => {
|
||||
it('filters out empty values', () => {
|
||||
const path = createNavigationPathWithParams('/datasets/123/documents', {
|
||||
page: 1,
|
||||
limit: '',
|
||||
|
|
@ -166,14 +166,14 @@ describe('Navigation Utilities', () => {
|
|||
expect(path).toBe('/datasets/123/documents?page=1&keyword=test')
|
||||
})
|
||||
|
||||
test('handles errors gracefully', () => {
|
||||
it('handles errors gracefully', () => {
|
||||
// Mock URLSearchParams to throw an error
|
||||
const originalURLSearchParams = globalThis.URLSearchParams
|
||||
globalThis.URLSearchParams = jest.fn(() => {
|
||||
globalThis.URLSearchParams = vi.fn(() => {
|
||||
throw new Error('URLSearchParams error')
|
||||
}) as any
|
||||
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { /* noop */ })
|
||||
const path = createNavigationPathWithParams('/datasets/123/documents', { page: 1 })
|
||||
|
||||
expect(path).toBe('/datasets/123/documents')
|
||||
|
|
@ -185,7 +185,7 @@ describe('Navigation Utilities', () => {
|
|||
})
|
||||
|
||||
describe('mergeQueryParams', () => {
|
||||
test('merges new params with existing ones', () => {
|
||||
it('merges new params with existing ones', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=3&limit=10' },
|
||||
writable: true,
|
||||
|
|
@ -199,7 +199,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(result).toContain('keyword=test') // added
|
||||
})
|
||||
|
||||
test('removes parameters when value is null', () => {
|
||||
it('removes parameters when value is null', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=3&limit=10&keyword=test' },
|
||||
writable: true,
|
||||
|
|
@ -214,7 +214,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(result).toContain('filter=active')
|
||||
})
|
||||
|
||||
test('creates fresh params when preserveExisting is false', () => {
|
||||
it('creates fresh params when preserveExisting is false', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=3&limit=10' },
|
||||
writable: true,
|
||||
|
|
@ -228,7 +228,7 @@ describe('Navigation Utilities', () => {
|
|||
})
|
||||
|
||||
describe('datasetNavigation', () => {
|
||||
test('backToDocuments creates correct navigation function', () => {
|
||||
it('backToDocuments creates correct navigation function', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=2&limit=25' },
|
||||
writable: true,
|
||||
|
|
@ -240,14 +240,14 @@ describe('Navigation Utilities', () => {
|
|||
expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-123/documents?page=2&limit=25')
|
||||
})
|
||||
|
||||
test('toDocumentDetail creates correct navigation function', () => {
|
||||
it('toDocumentDetail creates correct navigation function', () => {
|
||||
const detailFn = datasetNavigation.toDocumentDetail(mockRouter, 'dataset-123', 'doc-456')
|
||||
detailFn()
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-123/documents/doc-456')
|
||||
})
|
||||
|
||||
test('toDocumentSettings creates correct navigation function', () => {
|
||||
it('toDocumentSettings creates correct navigation function', () => {
|
||||
const settingsFn = datasetNavigation.toDocumentSettings(mockRouter, 'dataset-123', 'doc-456')
|
||||
settingsFn()
|
||||
|
||||
|
|
@ -256,7 +256,7 @@ describe('Navigation Utilities', () => {
|
|||
})
|
||||
|
||||
describe('Real-world Integration Scenarios', () => {
|
||||
test('complete user workflow: list -> detail -> back', () => {
|
||||
it('complete user workflow: list -> detail -> back', () => {
|
||||
// User starts on page 3 with search
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=3&keyword=API&limit=25' },
|
||||
|
|
@ -273,7 +273,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(mockPush).toHaveBeenCalledWith('/datasets/main-dataset/documents?page=3&keyword=API&limit=25')
|
||||
})
|
||||
|
||||
test('user applies filters then views document', () => {
|
||||
it('user applies filters then views document', () => {
|
||||
// Complex filter state
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=1&limit=50&status=active&type=pdf&sort=created_at&order=desc' },
|
||||
|
|
@ -288,7 +288,7 @@ describe('Navigation Utilities', () => {
|
|||
})
|
||||
|
||||
describe('Edge Cases and Error Handling', () => {
|
||||
test('handles special characters in query parameters', () => {
|
||||
it('handles special characters in query parameters', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?keyword=hello%20world&filter=type%3Apdf&tag=%E4%B8%AD%E6%96%87' },
|
||||
writable: true,
|
||||
|
|
@ -300,7 +300,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(path).toContain('%E4%B8%AD%E6%96%87')
|
||||
})
|
||||
|
||||
test('handles duplicate query parameters', () => {
|
||||
it('handles duplicate query parameters', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?tag=tag1&tag=tag2&tag=tag3' },
|
||||
writable: true,
|
||||
|
|
@ -311,7 +311,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(params.tag).toBe('tag1')
|
||||
})
|
||||
|
||||
test('handles very long query strings', () => {
|
||||
it('handles very long query strings', () => {
|
||||
const longValue = 'a'.repeat(1000)
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: `?data=${longValue}` },
|
||||
|
|
@ -323,7 +323,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(path.length).toBeGreaterThan(1000)
|
||||
})
|
||||
|
||||
test('handles empty string values in query parameters', () => {
|
||||
it('handles empty string values in query parameters', () => {
|
||||
const path = createNavigationPathWithParams('/datasets/123/documents', {
|
||||
page: 1,
|
||||
keyword: '',
|
||||
|
|
@ -336,7 +336,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(path).not.toContain('filter=')
|
||||
})
|
||||
|
||||
test('handles null and undefined values in mergeQueryParams', () => {
|
||||
it('handles null and undefined values in mergeQueryParams', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=1&limit=10&keyword=test' },
|
||||
writable: true,
|
||||
|
|
@ -355,7 +355,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(result).toContain('sort=name')
|
||||
})
|
||||
|
||||
test('handles navigation with hash fragments', () => {
|
||||
it('handles navigation with hash fragments', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=1', hash: '#section-2' },
|
||||
writable: true,
|
||||
|
|
@ -366,7 +366,7 @@ describe('Navigation Utilities', () => {
|
|||
expect(path).toBe('/datasets/123/documents?page=1')
|
||||
})
|
||||
|
||||
test('handles malformed query strings gracefully', () => {
|
||||
it('handles malformed query strings gracefully', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?page=1&invalid&limit=10&=value&key=' },
|
||||
writable: true,
|
||||
|
|
@ -382,7 +382,7 @@ describe('Navigation Utilities', () => {
|
|||
})
|
||||
|
||||
describe('Performance Tests', () => {
|
||||
test('handles large number of query parameters efficiently', () => {
|
||||
it('handles large number of query parameters efficiently', () => {
|
||||
const manyParams = Array.from({ length: 50 }, (_, i) => `param${i}=value${i}`).join('&')
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: `?${manyParams}` },
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useEffect, useState } from 'react'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
|
||||
const DARK_MODE_MEDIA_QUERY = /prefers-color-scheme:\s*dark/i
|
||||
|
||||
|
|
@ -76,14 +76,14 @@ const setupMockEnvironment = (storedTheme: string | null, systemPrefersDark = fa
|
|||
return mediaQueryList
|
||||
}
|
||||
|
||||
jest.spyOn(window, 'matchMedia').mockImplementation(mockMatchMedia)
|
||||
vi.spyOn(window, 'matchMedia').mockImplementation(mockMatchMedia)
|
||||
}
|
||||
|
||||
// Helper function to create timing page component
|
||||
const createTimingPageComponent = (
|
||||
timingData: Array<{ phase: string; timestamp: number; styles: { backgroundColor: string; color: string } }>,
|
||||
timingData: Array<{ phase: string, timestamp: number, styles: { backgroundColor: string, color: string } }>,
|
||||
) => {
|
||||
const recordTiming = (phase: string, styles: { backgroundColor: string; color: string }) => {
|
||||
const recordTiming = (phase: string, styles: { backgroundColor: string, color: string }) => {
|
||||
timingData.push({
|
||||
phase,
|
||||
timestamp: performance.now(),
|
||||
|
|
@ -113,7 +113,17 @@ const createTimingPageComponent = (
|
|||
style={currentStyles}
|
||||
>
|
||||
<div data-testid="timing-status">
|
||||
Phase: {mounted ? 'CSR' : 'Initial'} | Theme: {theme} | Visual: {isDark ? 'dark' : 'light'}
|
||||
Phase:
|
||||
{' '}
|
||||
{mounted ? 'CSR' : 'Initial'}
|
||||
{' '}
|
||||
| Theme:
|
||||
{' '}
|
||||
{theme}
|
||||
{' '}
|
||||
| Visual:
|
||||
{' '}
|
||||
{isDark ? 'dark' : 'light'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -124,7 +134,7 @@ const createTimingPageComponent = (
|
|||
|
||||
// Helper function to create CSS test component
|
||||
const createCSSTestComponent = (
|
||||
cssStates: Array<{ className: string; timestamp: number }>,
|
||||
cssStates: Array<{ className: string, timestamp: number }>,
|
||||
) => {
|
||||
const recordCSSState = (className: string) => {
|
||||
cssStates.push({
|
||||
|
|
@ -151,7 +161,10 @@ const createCSSTestComponent = (
|
|||
data-testid="css-component"
|
||||
className={className}
|
||||
>
|
||||
<div data-testid="css-classes">Classes: {className}</div>
|
||||
<div data-testid="css-classes">
|
||||
Classes:
|
||||
{className}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -161,7 +174,7 @@ const createCSSTestComponent = (
|
|||
|
||||
// Helper function to create performance test component
|
||||
const createPerformanceTestComponent = (
|
||||
performanceMarks: Array<{ event: string; timestamp: number }>,
|
||||
performanceMarks: Array<{ event: string, timestamp: number }>,
|
||||
) => {
|
||||
const recordPerformanceMark = (event: string) => {
|
||||
performanceMarks.push({ event, timestamp: performance.now() })
|
||||
|
|
@ -186,7 +199,13 @@ const createPerformanceTestComponent = (
|
|||
|
||||
return (
|
||||
<div data-testid="performance-test">
|
||||
Mounted: {mounted.toString()} | Theme: {theme || 'loading'}
|
||||
Mounted:
|
||||
{' '}
|
||||
{mounted.toString()}
|
||||
{' '}
|
||||
| Theme:
|
||||
{' '}
|
||||
{theme || 'loading'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -216,10 +235,14 @@ const PageComponent = () => {
|
|||
Dify Application
|
||||
</h1>
|
||||
<div data-testid="theme-indicator">
|
||||
Current Theme: {mounted ? theme : 'unknown'}
|
||||
Current Theme:
|
||||
{' '}
|
||||
{mounted ? theme : 'unknown'}
|
||||
</div>
|
||||
<div data-testid="visual-appearance">
|
||||
Appearance: {isDark ? 'dark' : 'light'}
|
||||
Appearance:
|
||||
{' '}
|
||||
{isDark ? 'dark' : 'light'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -240,8 +263,8 @@ const TestThemeProvider = ({ children }: { children: React.ReactNode }) => (
|
|||
|
||||
describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
jest.clearAllMocks()
|
||||
vi.restoreAllMocks()
|
||||
vi.clearAllMocks()
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
window.localStorage.clear()
|
||||
|
|
@ -254,7 +277,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
})
|
||||
|
||||
describe('Page Refresh Scenario Simulation', () => {
|
||||
test('simulates complete page loading process with dark theme', async () => {
|
||||
it('simulates complete page loading process with dark theme', async () => {
|
||||
// Setup: User previously selected dark mode
|
||||
setupMockEnvironment('dark')
|
||||
|
||||
|
|
@ -286,7 +309,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
console.log('State change detection: Initial -> Final')
|
||||
})
|
||||
|
||||
test('handles light theme correctly', async () => {
|
||||
it('handles light theme correctly', async () => {
|
||||
setupMockEnvironment('light')
|
||||
|
||||
render(
|
||||
|
|
@ -302,7 +325,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: light')
|
||||
})
|
||||
|
||||
test('handles system theme with dark preference', async () => {
|
||||
it('handles system theme with dark preference', async () => {
|
||||
setupMockEnvironment('system', true) // system theme, dark preference
|
||||
|
||||
render(
|
||||
|
|
@ -318,7 +341,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: dark')
|
||||
})
|
||||
|
||||
test('handles system theme with light preference', async () => {
|
||||
it('handles system theme with light preference', async () => {
|
||||
setupMockEnvironment('system', false) // system theme, light preference
|
||||
|
||||
render(
|
||||
|
|
@ -334,7 +357,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: light')
|
||||
})
|
||||
|
||||
test('handles no stored theme (defaults to system)', async () => {
|
||||
it('handles no stored theme (defaults to system)', async () => {
|
||||
setupMockEnvironment(null, false) // no stored theme, system prefers light
|
||||
|
||||
render(
|
||||
|
|
@ -348,10 +371,10 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('measures timing window of style changes', async () => {
|
||||
it('measures timing window of style changes', async () => {
|
||||
setupMockEnvironment('dark')
|
||||
|
||||
const timingData: Array<{ phase: string; timestamp: number; styles: any }> = []
|
||||
const timingData: Array<{ phase: string, timestamp: number, styles: any }> = []
|
||||
const TimingPageComponent = createTimingPageComponent(timingData)
|
||||
|
||||
render(
|
||||
|
|
@ -384,10 +407,10 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
})
|
||||
|
||||
describe('CSS Application Timing Tests', () => {
|
||||
test('checks CSS class changes causing flicker', async () => {
|
||||
it('checks CSS class changes causing flicker', async () => {
|
||||
setupMockEnvironment('dark')
|
||||
|
||||
const cssStates: Array<{ className: string; timestamp: number }> = []
|
||||
const cssStates: Array<{ className: string, timestamp: number }> = []
|
||||
const CSSTestComponent = createCSSTestComponent(cssStates)
|
||||
|
||||
render(
|
||||
|
|
@ -420,16 +443,16 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
})
|
||||
|
||||
describe('Edge Cases and Error Handling', () => {
|
||||
test('handles localStorage access errors gracefully', async () => {
|
||||
it('handles localStorage access errors gracefully', async () => {
|
||||
setupMockEnvironment(null)
|
||||
|
||||
const mockStorage = {
|
||||
getItem: jest.fn(() => {
|
||||
getItem: vi.fn(() => {
|
||||
throw new Error('LocalStorage access denied')
|
||||
}),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
|
|
@ -457,7 +480,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
}
|
||||
})
|
||||
|
||||
test('handles invalid theme values in localStorage', async () => {
|
||||
it('handles invalid theme values in localStorage', async () => {
|
||||
setupMockEnvironment('invalid-theme-value')
|
||||
|
||||
render(
|
||||
|
|
@ -477,8 +500,8 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
})
|
||||
|
||||
describe('Performance and Regression Tests', () => {
|
||||
test('verifies ThemeProvider position fix reduces initialization delay', async () => {
|
||||
const performanceMarks: Array<{ event: string; timestamp: number }> = []
|
||||
it('verifies ThemeProvider position fix reduces initialization delay', async () => {
|
||||
const performanceMarks: Array<{ event: string, timestamp: number }> = []
|
||||
|
||||
setupMockEnvironment('dark')
|
||||
|
||||
|
|
@ -507,7 +530,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
|||
})
|
||||
|
||||
describe('Solution Requirements Definition', () => {
|
||||
test('defines technical requirements to eliminate flicker', () => {
|
||||
it('defines technical requirements to eliminate flicker', () => {
|
||||
const technicalRequirements = {
|
||||
ssrConsistency: 'SSR and CSR must render identical initial styles',
|
||||
synchronousDetection: 'Theme detection must complete synchronously before first render',
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ describe('Unified Tags Editing - Pure Logic Tests', () => {
|
|||
// This is the valueNotChanged logic from TagSelector component
|
||||
const valueNotChanged
|
||||
= currentValue.length === newSelectedTagIDs.length
|
||||
&& currentValue.every(v => newSelectedTagIDs.includes(v))
|
||||
&& newSelectedTagIDs.every(v => currentValue.includes(v))
|
||||
&& currentValue.every(v => newSelectedTagIDs.includes(v))
|
||||
&& newSelectedTagIDs.every(v => currentValue.includes(v))
|
||||
|
||||
expect(valueNotChanged).toBe(false)
|
||||
})
|
||||
|
|
@ -26,8 +26,8 @@ describe('Unified Tags Editing - Pure Logic Tests', () => {
|
|||
|
||||
const valueNotChanged
|
||||
= currentValue.length === newSelectedTagIDs.length
|
||||
&& currentValue.every(v => newSelectedTagIDs.includes(v))
|
||||
&& newSelectedTagIDs.every(v => currentValue.includes(v))
|
||||
&& currentValue.every(v => newSelectedTagIDs.includes(v))
|
||||
&& newSelectedTagIDs.every(v => currentValue.includes(v))
|
||||
|
||||
expect(valueNotChanged).toBe(true)
|
||||
})
|
||||
|
|
@ -70,7 +70,7 @@ describe('Unified Tags Editing - Pure Logic Tests', () => {
|
|||
})
|
||||
|
||||
describe('Fallback Logic (from layout-main.tsx)', () => {
|
||||
type Tag = { id: string; name: string }
|
||||
type Tag = { id: string, name: string }
|
||||
type AppDetail = { tags: Tag[] }
|
||||
type FallbackResult = { tags?: Tag[] } | null
|
||||
// no-op
|
||||
|
|
@ -316,7 +316,7 @@ describe('Unified Tags Editing - Pure Logic Tests', () => {
|
|||
]
|
||||
|
||||
// Filter out invalid entries
|
||||
const validTags = mixedData.filter((tag): tag is { id: string; name: string; type: string; binding_count: number } =>
|
||||
const validTags = mixedData.filter((tag): tag is { id: string, name: string, type: string, binding_count: number } =>
|
||||
tag != null
|
||||
&& typeof tag === 'object'
|
||||
&& 'id' in tag
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { Mock } from 'vitest'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
// Type for mocked store
|
||||
type MockWorkflowStore = {
|
||||
showOnboarding: boolean
|
||||
setShowOnboarding: jest.Mock
|
||||
setShowOnboarding: Mock
|
||||
hasShownOnboarding: boolean
|
||||
setHasShownOnboarding: jest.Mock
|
||||
setHasShownOnboarding: Mock
|
||||
hasSelectedStartNode: boolean
|
||||
setHasSelectedStartNode: jest.Mock
|
||||
setShouldAutoOpenStartNodeSelector: jest.Mock
|
||||
setHasSelectedStartNode: Mock
|
||||
setShouldAutoOpenStartNodeSelector: Mock
|
||||
notInitialWorkflow: boolean
|
||||
}
|
||||
|
||||
|
|
@ -20,11 +21,11 @@ type MockNode = {
|
|||
}
|
||||
|
||||
// Mock zustand store
|
||||
jest.mock('@/app/components/workflow/store')
|
||||
vi.mock('@/app/components/workflow/store')
|
||||
|
||||
// Mock ReactFlow store
|
||||
const mockGetNodes = jest.fn()
|
||||
jest.mock('reactflow', () => ({
|
||||
const mockGetNodes = vi.fn()
|
||||
vi.mock('reactflow', () => ({
|
||||
useStoreApi: () => ({
|
||||
getState: () => ({
|
||||
getNodes: mockGetNodes,
|
||||
|
|
@ -33,16 +34,16 @@ jest.mock('reactflow', () => ({
|
|||
}))
|
||||
|
||||
describe('Workflow Onboarding Integration Logic', () => {
|
||||
const mockSetShowOnboarding = jest.fn()
|
||||
const mockSetHasSelectedStartNode = jest.fn()
|
||||
const mockSetHasShownOnboarding = jest.fn()
|
||||
const mockSetShouldAutoOpenStartNodeSelector = jest.fn()
|
||||
const mockSetShowOnboarding = vi.fn()
|
||||
const mockSetHasSelectedStartNode = vi.fn()
|
||||
const mockSetHasShownOnboarding = vi.fn()
|
||||
const mockSetShouldAutoOpenStartNodeSelector = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock store implementation
|
||||
;(useWorkflowStore as jest.Mock).mockReturnValue({
|
||||
;(useWorkflowStore as Mock).mockReturnValue({
|
||||
showOnboarding: false,
|
||||
setShowOnboarding: mockSetShowOnboarding,
|
||||
hasSelectedStartNode: false,
|
||||
|
|
@ -102,9 +103,9 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
|
||||
// Simulate the validation logic from use-nodes-sync-draft.ts
|
||||
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|
||||
expect(isValidStartNode).toBe(true)
|
||||
})
|
||||
|
|
@ -116,9 +117,9 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
}
|
||||
|
||||
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|
||||
expect(isValidStartNode).toBe(true)
|
||||
})
|
||||
|
|
@ -130,9 +131,9 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
}
|
||||
|
||||
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|
||||
expect(isValidStartNode).toBe(true)
|
||||
})
|
||||
|
|
@ -144,9 +145,9 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
}
|
||||
|
||||
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|
||||
expect(isValidStartNode).toBe(true)
|
||||
})
|
||||
|
|
@ -158,9 +159,9 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
}
|
||||
|
||||
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|
||||
expect(isValidStartNode).toBe(false)
|
||||
})
|
||||
|
|
@ -373,12 +374,12 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
it('should trigger onboarding for new workflow when draft does not exist', () => {
|
||||
// Simulate the error handling logic from use-workflow-init.ts
|
||||
const error = {
|
||||
json: jest.fn().mockResolvedValue({ code: 'draft_workflow_not_exist' }),
|
||||
json: vi.fn().mockResolvedValue({ code: 'draft_workflow_not_exist' }),
|
||||
bodyUsed: false,
|
||||
}
|
||||
|
||||
const mockWorkflowStore = {
|
||||
setState: jest.fn(),
|
||||
setState: vi.fn(),
|
||||
}
|
||||
|
||||
// Simulate error handling
|
||||
|
|
@ -404,7 +405,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
it('should not trigger onboarding for existing workflows', () => {
|
||||
// Simulate successful draft fetch
|
||||
const mockWorkflowStore = {
|
||||
setState: jest.fn(),
|
||||
setState: vi.fn(),
|
||||
}
|
||||
|
||||
// Normal initialization path should not set showOnboarding: true
|
||||
|
|
@ -419,7 +420,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
})
|
||||
|
||||
it('should create empty draft with proper structure', () => {
|
||||
const mockSyncWorkflowDraft = jest.fn()
|
||||
const mockSyncWorkflowDraft = vi.fn()
|
||||
const appId = 'test-app-id'
|
||||
|
||||
// Simulate the syncWorkflowDraft call from use-workflow-init.ts
|
||||
|
|
@ -467,7 +468,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
mockGetNodes.mockReturnValue([])
|
||||
|
||||
// Mock store with proper state for auto-detection
|
||||
;(useWorkflowStore as jest.Mock).mockReturnValue({
|
||||
;(useWorkflowStore as Mock).mockReturnValue({
|
||||
showOnboarding: false,
|
||||
hasShownOnboarding: false,
|
||||
notInitialWorkflow: false,
|
||||
|
|
@ -550,7 +551,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
mockGetNodes.mockReturnValue([])
|
||||
|
||||
// Mock store with hasShownOnboarding = true
|
||||
;(useWorkflowStore as jest.Mock).mockReturnValue({
|
||||
;(useWorkflowStore as Mock).mockReturnValue({
|
||||
showOnboarding: false,
|
||||
hasShownOnboarding: true, // Already shown in this session
|
||||
notInitialWorkflow: false,
|
||||
|
|
@ -584,7 +585,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
mockGetNodes.mockReturnValue([])
|
||||
|
||||
// Mock store with notInitialWorkflow = true (initial creation)
|
||||
;(useWorkflowStore as jest.Mock).mockReturnValue({
|
||||
;(useWorkflowStore as Mock).mockReturnValue({
|
||||
showOnboarding: false,
|
||||
hasShownOnboarding: false,
|
||||
notInitialWorkflow: true, // Initial workflow creation
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
|
||||
// Mock environment variables before importing constants
|
||||
const originalEnv = process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT
|
||||
|
|
@ -19,7 +19,7 @@ function setupEnvironment(value?: string) {
|
|||
delete process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT
|
||||
|
||||
// Clear module cache to force re-evaluation
|
||||
jest.resetModules()
|
||||
vi.resetModules()
|
||||
}
|
||||
|
||||
function restoreEnvironment() {
|
||||
|
|
@ -28,37 +28,42 @@ function restoreEnvironment() {
|
|||
else
|
||||
delete process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT
|
||||
|
||||
jest.resetModules()
|
||||
vi.resetModules()
|
||||
}
|
||||
|
||||
// Mock i18next with proper implementation
|
||||
jest.mock('react-i18next', () => ({
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
if (key.includes('MaxParallelismTitle')) return 'Max Parallelism'
|
||||
if (key.includes('MaxParallelismDesc')) return 'Maximum number of parallel executions'
|
||||
if (key.includes('parallelMode')) return 'Parallel Mode'
|
||||
if (key.includes('parallelPanelDesc')) return 'Enable parallel execution'
|
||||
if (key.includes('errorResponseMethod')) return 'Error Response Method'
|
||||
if (key.includes('MaxParallelismTitle'))
|
||||
return 'Max Parallelism'
|
||||
if (key.includes('MaxParallelismDesc'))
|
||||
return 'Maximum number of parallel executions'
|
||||
if (key.includes('parallelMode'))
|
||||
return 'Parallel Mode'
|
||||
if (key.includes('parallelPanelDesc'))
|
||||
return 'Enable parallel execution'
|
||||
if (key.includes('errorResponseMethod'))
|
||||
return 'Error Response Method'
|
||||
return key
|
||||
},
|
||||
}),
|
||||
initReactI18next: {
|
||||
type: '3rdParty',
|
||||
init: jest.fn(),
|
||||
init: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock i18next module completely to prevent initialization issues
|
||||
jest.mock('i18next', () => ({
|
||||
use: jest.fn().mockReturnThis(),
|
||||
init: jest.fn().mockReturnThis(),
|
||||
t: jest.fn(key => key),
|
||||
vi.mock('i18next', () => ({
|
||||
use: vi.fn().mockReturnThis(),
|
||||
init: vi.fn().mockReturnThis(),
|
||||
t: vi.fn(key => key),
|
||||
isInitialized: true,
|
||||
}))
|
||||
|
||||
// Mock the useConfig hook
|
||||
jest.mock('@/app/components/workflow/nodes/iteration/use-config', () => ({
|
||||
vi.mock('@/app/components/workflow/nodes/iteration/use-config', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
inputs: {
|
||||
|
|
@ -66,82 +71,39 @@ jest.mock('@/app/components/workflow/nodes/iteration/use-config', () => ({
|
|||
parallel_nums: 5,
|
||||
error_handle_mode: 'terminated',
|
||||
},
|
||||
changeParallel: jest.fn(),
|
||||
changeParallelNums: jest.fn(),
|
||||
changeErrorHandleMode: jest.fn(),
|
||||
changeParallel: vi.fn(),
|
||||
changeParallelNums: vi.fn(),
|
||||
changeErrorHandleMode: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock other components
|
||||
jest.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => {
|
||||
return function MockVarReferencePicker() {
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => ({
|
||||
default: function MockVarReferencePicker() {
|
||||
return <div data-testid="var-reference-picker">VarReferencePicker</div>
|
||||
}
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/workflow/nodes/_base/components/split', () => {
|
||||
return function MockSplit() {
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/split', () => ({
|
||||
default: function MockSplit() {
|
||||
return <div data-testid="split">Split</div>
|
||||
}
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/workflow/nodes/_base/components/field', () => {
|
||||
return function MockField({ title, children }: { title: string, children: React.ReactNode }) {
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/field', () => ({
|
||||
default: function MockField({ title, children }: { title: string, children: React.ReactNode }) {
|
||||
return (
|
||||
<div data-testid="field">
|
||||
<label>{title}</label>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/base/switch', () => {
|
||||
return function MockSwitch({ defaultValue }: { defaultValue: boolean }) {
|
||||
return <input type="checkbox" defaultChecked={defaultValue} data-testid="switch" />
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/app/components/base/select', () => {
|
||||
return function MockSelect() {
|
||||
return <select data-testid="select">Select</select>
|
||||
}
|
||||
})
|
||||
|
||||
// Use defaultValue to avoid controlled input warnings
|
||||
jest.mock('@/app/components/base/slider', () => {
|
||||
return function MockSlider({ value, max, min }: { value: number, max: number, min: number }) {
|
||||
return (
|
||||
<input
|
||||
type="range"
|
||||
defaultValue={value}
|
||||
max={max}
|
||||
min={min}
|
||||
data-testid="slider"
|
||||
data-max={max}
|
||||
data-min={min}
|
||||
readOnly
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Use defaultValue to avoid controlled input warnings
|
||||
jest.mock('@/app/components/base/input', () => {
|
||||
return function MockInput({ type, max, min, value }: { type: string, max: number, min: number, value: number }) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
defaultValue={value}
|
||||
max={max}
|
||||
min={min}
|
||||
data-testid="number-input"
|
||||
data-max={max}
|
||||
data-min={min}
|
||||
readOnly
|
||||
/>
|
||||
)
|
||||
}
|
||||
const getParallelControls = () => ({
|
||||
numberInput: screen.getByRole('spinbutton'),
|
||||
slider: screen.getByRole('slider'),
|
||||
})
|
||||
|
||||
describe('MAX_PARALLEL_LIMIT Configuration Bug', () => {
|
||||
|
|
@ -160,7 +122,7 @@ describe('MAX_PARALLEL_LIMIT Configuration Bug', () => {
|
|||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -172,115 +134,114 @@ describe('MAX_PARALLEL_LIMIT Configuration Bug', () => {
|
|||
})
|
||||
|
||||
describe('Environment Variable Parsing', () => {
|
||||
it('should parse MAX_PARALLEL_LIMIT from NEXT_PUBLIC_MAX_PARALLEL_LIMIT environment variable', () => {
|
||||
it('should parse MAX_PARALLEL_LIMIT from NEXT_PUBLIC_MAX_PARALLEL_LIMIT environment variable', async () => {
|
||||
setupEnvironment('25')
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
const { MAX_PARALLEL_LIMIT } = await import('@/config')
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(25)
|
||||
})
|
||||
|
||||
it('should fallback to default when environment variable is not set', () => {
|
||||
it('should fallback to default when environment variable is not set', async () => {
|
||||
setupEnvironment() // No environment variable
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
const { MAX_PARALLEL_LIMIT } = await import('@/config')
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(10)
|
||||
})
|
||||
|
||||
it('should handle invalid environment variable values', () => {
|
||||
it('should handle invalid environment variable values', async () => {
|
||||
setupEnvironment('invalid')
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
const { MAX_PARALLEL_LIMIT } = await import('@/config')
|
||||
|
||||
// Should fall back to default when parsing fails
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(10)
|
||||
})
|
||||
|
||||
it('should handle empty environment variable', () => {
|
||||
it('should handle empty environment variable', async () => {
|
||||
setupEnvironment('')
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
const { MAX_PARALLEL_LIMIT } = await import('@/config')
|
||||
|
||||
// Should fall back to default when empty
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(10)
|
||||
})
|
||||
|
||||
// Edge cases for boundary values
|
||||
it('should clamp MAX_PARALLEL_LIMIT to MIN when env is 0 or negative', () => {
|
||||
it('should clamp MAX_PARALLEL_LIMIT to MIN when env is 0 or negative', async () => {
|
||||
setupEnvironment('0')
|
||||
let { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
let { MAX_PARALLEL_LIMIT } = await import('@/config')
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(10) // Falls back to default
|
||||
|
||||
setupEnvironment('-5')
|
||||
;({ MAX_PARALLEL_LIMIT } = require('@/config'))
|
||||
;({ MAX_PARALLEL_LIMIT } = await import('@/config'))
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(10) // Falls back to default
|
||||
})
|
||||
|
||||
it('should handle float numbers by parseInt behavior', () => {
|
||||
it('should handle float numbers by parseInt behavior', async () => {
|
||||
setupEnvironment('12.7')
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
const { MAX_PARALLEL_LIMIT } = await import('@/config')
|
||||
// parseInt truncates to integer
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(12)
|
||||
})
|
||||
})
|
||||
|
||||
describe('UI Component Integration (Main Fix Verification)', () => {
|
||||
it('should render iteration panel with environment-configured max value', () => {
|
||||
it('should render iteration panel with environment-configured max value', async () => {
|
||||
// Set environment variable to a different value
|
||||
setupEnvironment('30')
|
||||
|
||||
// Import Panel after setting environment
|
||||
const Panel = require('@/app/components/workflow/nodes/iteration/panel').default
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
const Panel = await import('@/app/components/workflow/nodes/iteration/panel').then(mod => mod.default)
|
||||
const { MAX_PARALLEL_LIMIT } = await import('@/config')
|
||||
|
||||
render(
|
||||
<Panel
|
||||
id="test-node"
|
||||
// @ts-expect-error key type mismatch
|
||||
data={mockNodeData.data}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Behavior-focused assertion: UI max should equal MAX_PARALLEL_LIMIT
|
||||
const numberInput = screen.getByTestId('number-input')
|
||||
expect(numberInput).toHaveAttribute('data-max', String(MAX_PARALLEL_LIMIT))
|
||||
|
||||
const slider = screen.getByTestId('slider')
|
||||
expect(slider).toHaveAttribute('data-max', String(MAX_PARALLEL_LIMIT))
|
||||
const { numberInput, slider } = getParallelControls()
|
||||
expect(numberInput).toHaveAttribute('max', String(MAX_PARALLEL_LIMIT))
|
||||
expect(slider).toHaveAttribute('aria-valuemax', String(MAX_PARALLEL_LIMIT))
|
||||
|
||||
// Verify the actual values
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(30)
|
||||
expect(numberInput.getAttribute('data-max')).toBe('30')
|
||||
expect(slider.getAttribute('data-max')).toBe('30')
|
||||
expect(numberInput.getAttribute('max')).toBe('30')
|
||||
expect(slider.getAttribute('aria-valuemax')).toBe('30')
|
||||
})
|
||||
|
||||
it('should maintain UI consistency with different environment values', () => {
|
||||
it('should maintain UI consistency with different environment values', async () => {
|
||||
setupEnvironment('15')
|
||||
const Panel = require('@/app/components/workflow/nodes/iteration/panel').default
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
const Panel = await import('@/app/components/workflow/nodes/iteration/panel').then(mod => mod.default)
|
||||
const { MAX_PARALLEL_LIMIT } = await import('@/config')
|
||||
|
||||
render(
|
||||
<Panel
|
||||
id="test-node"
|
||||
// @ts-expect-error key type mismatch
|
||||
data={mockNodeData.data}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Both input and slider should use the same max value from MAX_PARALLEL_LIMIT
|
||||
const numberInput = screen.getByTestId('number-input')
|
||||
const slider = screen.getByTestId('slider')
|
||||
const { numberInput, slider } = getParallelControls()
|
||||
|
||||
expect(numberInput.getAttribute('data-max')).toBe(slider.getAttribute('data-max'))
|
||||
expect(numberInput.getAttribute('data-max')).toBe(String(MAX_PARALLEL_LIMIT))
|
||||
expect(numberInput.getAttribute('max')).toBe(slider.getAttribute('aria-valuemax'))
|
||||
expect(numberInput.getAttribute('max')).toBe(String(MAX_PARALLEL_LIMIT))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Legacy Constant Verification (For Transition Period)', () => {
|
||||
// Marked as transition/deprecation tests
|
||||
it('should maintain MAX_ITERATION_PARALLEL_NUM for backward compatibility', () => {
|
||||
const { MAX_ITERATION_PARALLEL_NUM } = require('@/app/components/workflow/constants')
|
||||
it('should maintain MAX_ITERATION_PARALLEL_NUM for backward compatibility', async () => {
|
||||
const { MAX_ITERATION_PARALLEL_NUM } = await import('@/app/components/workflow/constants')
|
||||
expect(typeof MAX_ITERATION_PARALLEL_NUM).toBe('number')
|
||||
expect(MAX_ITERATION_PARALLEL_NUM).toBe(10) // Hardcoded legacy value
|
||||
})
|
||||
|
||||
it('should demonstrate MAX_PARALLEL_LIMIT vs legacy constant difference', () => {
|
||||
it('should demonstrate MAX_PARALLEL_LIMIT vs legacy constant difference', async () => {
|
||||
setupEnvironment('50')
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
const { MAX_ITERATION_PARALLEL_NUM } = require('@/app/components/workflow/constants')
|
||||
const { MAX_PARALLEL_LIMIT } = await import('@/config')
|
||||
const { MAX_ITERATION_PARALLEL_NUM } = await import('@/app/components/workflow/constants')
|
||||
|
||||
// MAX_PARALLEL_LIMIT is configurable, MAX_ITERATION_PARALLEL_NUM is not
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(50)
|
||||
|
|
@ -290,9 +251,9 @@ describe('MAX_PARALLEL_LIMIT Configuration Bug', () => {
|
|||
})
|
||||
|
||||
describe('Constants Validation', () => {
|
||||
it('should validate that required constants exist and have correct types', () => {
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
const { MIN_ITERATION_PARALLEL_NUM } = require('@/app/components/workflow/constants')
|
||||
it('should validate that required constants exist and have correct types', async () => {
|
||||
const { MAX_PARALLEL_LIMIT } = await import('@/config')
|
||||
const { MIN_ITERATION_PARALLEL_NUM } = await import('@/app/components/workflow/constants')
|
||||
expect(typeof MAX_PARALLEL_LIMIT).toBe('number')
|
||||
expect(typeof MIN_ITERATION_PARALLEL_NUM).toBe('number')
|
||||
expect(MAX_PARALLEL_LIMIT).toBeGreaterThanOrEqual(MIN_ITERATION_PARALLEL_NUM)
|
||||
|
|
|
|||
|
|
@ -5,15 +5,16 @@
|
|||
* components have been properly fixed by replacing dangerouslySetInnerHTML with safe React rendering.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import * as React from 'react'
|
||||
import BlockInput from '../app/components/base/block-input'
|
||||
import SupportVarInput from '../app/components/workflow/nodes/_base/components/support-var-input'
|
||||
|
||||
// Mock styles
|
||||
jest.mock('../app/components/app/configuration/base/var-highlight/style.module.css', () => ({
|
||||
item: 'mock-item-class',
|
||||
vi.mock('../app/components/app/configuration/base/var-highlight/style.module.css', () => ({
|
||||
default: {
|
||||
item: 'mock-item-class',
|
||||
},
|
||||
}))
|
||||
|
||||
describe('XSS Prevention - Block Input and Support Var Input Security', () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import Main from '@/app/components/app/log-annotation'
|
||||
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import Configuration from '@/app/components/app/configuration'
|
||||
|
||||
const IConfiguration = async () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import * as React from 'react'
|
||||
import DevelopMain from '@/app/components/develop'
|
||||
|
||||
export type IDevelopProps = {
|
||||
params: Promise<{ locale: Locale; appId: string }>
|
||||
params: Promise<{ locale: Locale, appId: string }>
|
||||
}
|
||||
|
||||
const Develop = async (props: IDevelopProps) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useUnmount } from 'ahooks'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
|
||||
import type { App } from '@/types/app'
|
||||
import {
|
||||
RiDashboard2Fill,
|
||||
RiDashboard2Line,
|
||||
|
|
@ -13,21 +12,24 @@ import {
|
|||
RiTerminalWindowFill,
|
||||
RiTerminalWindowLine,
|
||||
} from '@remixicon/react'
|
||||
import { useUnmount } from 'ahooks'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
|
||||
import { fetchAppDetailDirect } from '@/service/apps'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { type App, AppModeEnum } from '@/types/app'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { fetchAppDetailDirect } from '@/service/apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import s from './style.module.css'
|
||||
|
||||
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
|
||||
ssr: false,
|
||||
|
|
@ -68,32 +70,32 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
const navConfig = [
|
||||
...(isCurrentWorkspaceEditor
|
||||
? [{
|
||||
name: t('common.appMenus.promptEng'),
|
||||
href: `/app/${appId}/${(mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.ADVANCED_CHAT) ? 'workflow' : 'configuration'}`,
|
||||
icon: RiTerminalWindowLine,
|
||||
selectedIcon: RiTerminalWindowFill,
|
||||
}]
|
||||
name: t('appMenus.promptEng', { ns: 'common' }),
|
||||
href: `/app/${appId}/${(mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.ADVANCED_CHAT) ? 'workflow' : 'configuration'}`,
|
||||
icon: RiTerminalWindowLine,
|
||||
selectedIcon: RiTerminalWindowFill,
|
||||
}]
|
||||
: []
|
||||
),
|
||||
{
|
||||
name: t('common.appMenus.apiAccess'),
|
||||
name: t('appMenus.apiAccess', { ns: 'common' }),
|
||||
href: `/app/${appId}/develop`,
|
||||
icon: RiTerminalBoxLine,
|
||||
selectedIcon: RiTerminalBoxFill,
|
||||
},
|
||||
...(isCurrentWorkspaceEditor
|
||||
? [{
|
||||
name: mode !== AppModeEnum.WORKFLOW
|
||||
? t('common.appMenus.logAndAnn')
|
||||
: t('common.appMenus.logs'),
|
||||
href: `/app/${appId}/logs`,
|
||||
icon: RiFileList3Line,
|
||||
selectedIcon: RiFileList3Fill,
|
||||
}]
|
||||
name: mode !== AppModeEnum.WORKFLOW
|
||||
? t('appMenus.logAndAnn', { ns: 'common' })
|
||||
: t('appMenus.logs', { ns: 'common' }),
|
||||
href: `/app/${appId}/logs`,
|
||||
icon: RiFileList3Line,
|
||||
selectedIcon: RiFileList3Fill,
|
||||
}]
|
||||
: []
|
||||
),
|
||||
{
|
||||
name: t('common.appMenus.overview'),
|
||||
name: t('appMenus.overview', { ns: 'common' }),
|
||||
href: `/app/${appId}/overview`,
|
||||
icon: RiDashboard2Line,
|
||||
selectedIcon: RiDashboard2Fill,
|
||||
|
|
@ -102,7 +104,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
return navConfig
|
||||
}, [t])
|
||||
|
||||
useDocumentTitle(appDetail?.name || t('common.menus.appDetail'))
|
||||
useDocumentTitle(appDetail?.name || t('menus.appDetail', { ns: 'common' }))
|
||||
|
||||
useEffect(() => {
|
||||
if (appDetail) {
|
||||
|
|
@ -156,7 +158,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
|
||||
if (!appDetail) {
|
||||
return (
|
||||
<div className='flex h-full items-center justify-center bg-background-body'>
|
||||
<div className="flex h-full items-center justify-center bg-background-body">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
|
|
@ -173,7 +175,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
{children}
|
||||
</div>
|
||||
{showTagManagementModal && (
|
||||
<TagManagementModal type='app' show={showTagManagementModal} />
|
||||
<TagManagementModal type="app" show={showTagManagementModal} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import Main from '@/app/components/app/log-annotation'
|
||||
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,32 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import type { IAppCardProps } from '@/app/components/app/overview/app-card'
|
||||
import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { UpdateAppSiteCodeResponse } from '@/models/app'
|
||||
import type { App } from '@/types/app'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import AppCard from '@/app/components/app/overview/app-card'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
||||
import TriggerCard from '@/app/components/app/overview/trigger-card'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
||||
import { isTriggerNode } from '@/app/components/workflow/types'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import {
|
||||
fetchAppDetail,
|
||||
updateAppSiteAccessToken,
|
||||
updateAppSiteConfig,
|
||||
updateAppSiteStatus,
|
||||
} from '@/service/apps'
|
||||
import type { App } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import type { UpdateAppSiteCodeResponse } from '@/models/app'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import type { IAppCardProps } from '@/app/components/app/overview/app-card'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useAppWorkflow } from '@/service/use-workflow'
|
||||
import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { isTriggerNode } from '@/app/components/workflow/types'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
|
||||
export type ICardViewProps = {
|
||||
appId: string
|
||||
|
|
@ -59,30 +61,30 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||
|
||||
const triggerDocUrl = docLink('/guides/workflow/node/start')
|
||||
const buildTriggerModeMessage = useCallback((featureName: string) => (
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='text-xs text-text-secondary'>
|
||||
{t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-xs text-text-secondary">
|
||||
{t('overview.disableTooltip.triggerMode', { ns: 'appOverview', feature: featureName })}
|
||||
</div>
|
||||
<div
|
||||
className='cursor-pointer text-xs font-medium text-text-accent hover:underline'
|
||||
className="cursor-pointer text-xs font-medium text-text-accent hover:underline"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
window.open(triggerDocUrl, '_blank')
|
||||
}}
|
||||
>
|
||||
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
|
||||
{t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
|
||||
</div>
|
||||
</div>
|
||||
), [t, triggerDocUrl])
|
||||
|
||||
const disableWebAppTooltip = disableAppCards
|
||||
? buildTriggerModeMessage(t('appOverview.overview.appInfo.title'))
|
||||
? buildTriggerModeMessage(t('overview.appInfo.title', { ns: 'appOverview' }))
|
||||
: null
|
||||
const disableApiTooltip = disableAppCards
|
||||
? buildTriggerModeMessage(t('appOverview.overview.apiInfo.title'))
|
||||
? buildTriggerModeMessage(t('overview.apiInfo.title', { ns: 'appOverview' }))
|
||||
: null
|
||||
const disableMcpTooltip = disableAppCards
|
||||
? buildTriggerModeMessage(t('tools.mcp.server.title'))
|
||||
? buildTriggerModeMessage(t('mcp.server.title', { ns: 'tools' }))
|
||||
: null
|
||||
|
||||
const updateAppDetail = async () => {
|
||||
|
|
@ -93,7 +95,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||
catch (error) { console.error(error) }
|
||||
}
|
||||
|
||||
const handleCallbackResult = (err: Error | null, message?: string) => {
|
||||
const handleCallbackResult = (err: Error | null, message?: I18nKeysByPrefix<'common', 'actionMsg.'>) => {
|
||||
const type = err ? 'error' : 'success'
|
||||
|
||||
message ||= (type === 'success' ? 'modifiedSuccessfully' : 'modifiedUnsuccessfully')
|
||||
|
|
@ -103,7 +105,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||
|
||||
notify({
|
||||
type,
|
||||
message: t(`common.actionMsg.${message}`),
|
||||
message: t(`actionMsg.${message}`, { ns: 'common' }) as string,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -185,12 +187,14 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||
</>
|
||||
)
|
||||
|
||||
const triggerCardNode = showTriggerCard ? (
|
||||
<TriggerCard
|
||||
appInfo={appDetail}
|
||||
onToggleResult={handleCallbackResult}
|
||||
/>
|
||||
) : null
|
||||
const triggerCardNode = showTriggerCard
|
||||
? (
|
||||
<TriggerCard
|
||||
appInfo={appDetail}
|
||||
onToggleResult={handleCallbackResult}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,25 @@
|
|||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import dayjs from 'dayjs'
|
||||
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
||||
import { TIME_PERIOD_MAPPING as LONG_TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter'
|
||||
import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/app-chart'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import TimeRangePicker from './time-range-picker'
|
||||
import { TIME_PERIOD_MAPPING as LONG_TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import LongTimeRangePicker from './long-time-range-picker'
|
||||
import TimeRangePicker from './time-range-picker'
|
||||
|
||||
dayjs.extend(quarterOfYear)
|
||||
|
||||
const today = dayjs()
|
||||
|
||||
const TIME_PERIOD_MAPPING = [
|
||||
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
|
||||
|
||||
const TIME_PERIOD_MAPPING: { value: number, name: TimePeriodName }[] = [
|
||||
{ value: 0, name: 'today' },
|
||||
{ value: 7, name: 'last7days' },
|
||||
{ value: 30, name: 'last30days' },
|
||||
|
|
@ -34,8 +38,8 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
|||
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
|
||||
const isWorkflow = appDetail?.mode === 'workflow'
|
||||
const [period, setPeriod] = useState<PeriodParams>(IS_CLOUD_EDITION
|
||||
? { name: t('appLog.filter.period.today'), query: { start: today.startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }
|
||||
: { name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } },
|
||||
? { name: t('filter.period.today', { ns: 'appLog' }), query: { start: today.startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }
|
||||
: { name: t('filter.period.last7days', { ns: 'appLog' }), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } },
|
||||
)
|
||||
|
||||
if (!appDetail)
|
||||
|
|
@ -43,63 +47,65 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-4'>
|
||||
<div className='system-xl-semibold mb-2 text-text-primary'>{t('common.appMenus.overview')}</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
{IS_CLOUD_EDITION ? (
|
||||
<TimeRangePicker
|
||||
ranges={TIME_PERIOD_MAPPING}
|
||||
onSelect={setPeriod}
|
||||
queryDateFormat={queryDateFormat}
|
||||
/>
|
||||
) : (
|
||||
<LongTimeRangePicker
|
||||
periodMapping={LONG_TIME_PERIOD_MAPPING}
|
||||
onSelect={setPeriod}
|
||||
queryDateFormat={queryDateFormat}
|
||||
/>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<div className="system-xl-semibold mb-2 text-text-primary">{t('appMenus.overview', { ns: 'common' })}</div>
|
||||
<div className="flex items-center justify-between">
|
||||
{IS_CLOUD_EDITION
|
||||
? (
|
||||
<TimeRangePicker
|
||||
ranges={TIME_PERIOD_MAPPING}
|
||||
onSelect={setPeriod}
|
||||
queryDateFormat={queryDateFormat}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<LongTimeRangePicker
|
||||
periodMapping={LONG_TIME_PERIOD_MAPPING}
|
||||
onSelect={setPeriod}
|
||||
queryDateFormat={queryDateFormat}
|
||||
/>
|
||||
)}
|
||||
|
||||
{headerRight}
|
||||
</div>
|
||||
</div>
|
||||
{!isWorkflow && (
|
||||
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||
<div className="mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2">
|
||||
<ConversationsChart period={period} id={appId} />
|
||||
<EndUsersChart period={period} id={appId} />
|
||||
</div>
|
||||
)}
|
||||
{!isWorkflow && (
|
||||
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||
<div className="mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2">
|
||||
{isChatApp
|
||||
? (
|
||||
<AvgSessionInteractions period={period} id={appId} />
|
||||
)
|
||||
<AvgSessionInteractions period={period} id={appId} />
|
||||
)
|
||||
: (
|
||||
<AvgResponseTime period={period} id={appId} />
|
||||
)}
|
||||
<AvgResponseTime period={period} id={appId} />
|
||||
)}
|
||||
<TokenPerSecond period={period} id={appId} />
|
||||
</div>
|
||||
)}
|
||||
{!isWorkflow && (
|
||||
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||
<div className="mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2">
|
||||
<UserSatisfactionRate period={period} id={appId} />
|
||||
<CostChart period={period} id={appId} />
|
||||
</div>
|
||||
)}
|
||||
{!isWorkflow && isChatApp && (
|
||||
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||
<div className="mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2">
|
||||
<MessagesChart period={period} id={appId} />
|
||||
</div>
|
||||
)}
|
||||
{isWorkflow && (
|
||||
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||
<div className="mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2">
|
||||
<WorkflowMessagesChart period={period} id={appId} />
|
||||
<WorkflowDailyTerminalsChart period={period} id={appId} />
|
||||
</div>
|
||||
)}
|
||||
{isWorkflow && (
|
||||
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||
<div className="mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2">
|
||||
<WorkflowCostChart period={period} id={appId} />
|
||||
<AvgUserInteractions period={period} id={appId} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
'use client'
|
||||
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import dayjs from 'dayjs'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
|
||||
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
|
||||
|
||||
type Props = {
|
||||
periodMapping: { [key: string]: { value: number; name: string } }
|
||||
periodMapping: { [key: string]: { value: number, name: TimePeriodName } }
|
||||
onSelect: (payload: PeriodParams) => void
|
||||
queryDateFormat: string
|
||||
}
|
||||
|
|
@ -24,9 +28,9 @@ const LongTimeRangePicker: FC<Props> = ({
|
|||
const handleSelect = React.useCallback((item: Item) => {
|
||||
const id = item.value
|
||||
const value = periodMapping[id]?.value ?? '-1'
|
||||
const name = item.name || t('appLog.filter.period.allTime')
|
||||
const name = item.name || t('filter.period.allTime', { ns: 'appLog' })
|
||||
if (value === -1) {
|
||||
onSelect({ name: t('appLog.filter.period.allTime'), query: undefined })
|
||||
onSelect({ name: t('filter.period.allTime', { ns: 'appLog' }), query: undefined })
|
||||
}
|
||||
else if (value === 0) {
|
||||
const startOfToday = today.startOf('day').format(queryDateFormat)
|
||||
|
|
@ -52,11 +56,11 @@ const LongTimeRangePicker: FC<Props> = ({
|
|||
|
||||
return (
|
||||
<SimpleSelect
|
||||
items={Object.entries(periodMapping).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}
|
||||
className='mt-0 !w-40'
|
||||
items={Object.entries(periodMapping).map(([k, v]) => ({ value: k, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))}
|
||||
className="mt-0 !w-40"
|
||||
notClearable={true}
|
||||
onSelect={handleSelect}
|
||||
defaultValue={'2'}
|
||||
defaultValue="2"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
|
||||
import ChartView from './chart-view'
|
||||
import TracingPanel from './tracing/panel'
|
||||
import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
|
||||
|
||||
export type IDevelopProps = {
|
||||
params: Promise<{ appId: string }>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
'use client'
|
||||
import { RiCalendarLine } from '@remixicon/react'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { formatToLocalTime } from '@/utils/format'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import Picker from '@/app/components/base/date-and-time-picker/date-picker'
|
||||
import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types'
|
||||
import { noop } from 'lodash-es'
|
||||
import { RiCalendarLine } from '@remixicon/react'
|
||||
import dayjs from 'dayjs'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import Picker from '@/app/components/base/date-and-time-picker/date-picker'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { formatToLocalTime } from '@/utils/format'
|
||||
|
||||
type Props = {
|
||||
start: Dayjs
|
||||
|
|
@ -50,9 +51,9 @@ const DatePicker: FC<Props> = ({
|
|||
}, [availableEndDate, start])
|
||||
|
||||
return (
|
||||
<div className='flex h-8 items-center space-x-0.5 rounded-lg bg-components-input-bg-normal px-2'>
|
||||
<div className='p-px'>
|
||||
<RiCalendarLine className='size-3.5 text-text-tertiary' />
|
||||
<div className="flex h-8 items-center space-x-0.5 rounded-lg bg-components-input-bg-normal px-2">
|
||||
<div className="p-px">
|
||||
<RiCalendarLine className="size-3.5 text-text-tertiary" />
|
||||
</div>
|
||||
<Picker
|
||||
value={start}
|
||||
|
|
@ -63,7 +64,7 @@ const DatePicker: FC<Props> = ({
|
|||
noConfirm
|
||||
getIsDateDisabled={startDateDisabled}
|
||||
/>
|
||||
<span className='system-sm-regular text-text-tertiary'>-</span>
|
||||
<span className="system-sm-regular text-text-tertiary">-</span>
|
||||
<Picker
|
||||
value={end}
|
||||
onChange={onEndChange}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
'use client'
|
||||
import type { PeriodParams, PeriodParamsWithTimeRange } from '@/app/components/app/overview/app-chart'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { HourglassShape } from '@/app/components/base/icons/src/vender/other'
|
||||
import RangeSelector from './range-selector'
|
||||
import DatePicker from './date-picker'
|
||||
import type { FC } from 'react'
|
||||
import type { PeriodParams, PeriodParamsWithTimeRange } from '@/app/components/app/overview/app-chart'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import dayjs from 'dayjs'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { HourglassShape } from '@/app/components/base/icons/src/vender/other'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { formatToLocalTime } from '@/utils/format'
|
||||
import DatePicker from './date-picker'
|
||||
import RangeSelector from './range-selector'
|
||||
|
||||
const today = dayjs()
|
||||
|
||||
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
|
||||
|
||||
type Props = {
|
||||
ranges: { value: number; name: string }[]
|
||||
ranges: { value: number, name: TimePeriodName }[]
|
||||
onSelect: (payload: PeriodParams) => void
|
||||
queryDateFormat: string
|
||||
}
|
||||
|
|
@ -44,9 +48,12 @@ const TimeRangePicker: FC<Props> = ({
|
|||
|
||||
const handleDateChange = useCallback((type: 'start' | 'end') => {
|
||||
return (date?: Dayjs) => {
|
||||
if (!date) return
|
||||
if (type === 'start' && date.isSame(start)) return
|
||||
if (type === 'end' && date.isSame(end)) return
|
||||
if (!date)
|
||||
return
|
||||
if (type === 'start' && date.isSame(start))
|
||||
return
|
||||
if (type === 'end' && date.isSame(end))
|
||||
return
|
||||
if (type === 'start')
|
||||
setStart(date)
|
||||
else
|
||||
|
|
@ -67,13 +74,13 @@ const TimeRangePicker: FC<Props> = ({
|
|||
}, [start, end, onSelect, locale, queryDateFormat])
|
||||
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<div className="flex items-center">
|
||||
<RangeSelector
|
||||
isCustomRange={isCustomRange}
|
||||
ranges={ranges}
|
||||
onSelect={handleRangeChange}
|
||||
/>
|
||||
<HourglassShape className='h-3.5 w-2 text-components-input-bg-normal' />
|
||||
<HourglassShape className="h-3.5 w-2 text-components-input-bg-normal" />
|
||||
<DatePicker
|
||||
start={start}
|
||||
end={end}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
'use client'
|
||||
import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import dayjs from 'dayjs'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import dayjs from 'dayjs'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const today = dayjs()
|
||||
|
||||
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>
|
||||
|
||||
type Props = {
|
||||
isCustomRange: boolean
|
||||
ranges: { value: number; name: string }[]
|
||||
ranges: { value: number, name: TimePeriodName }[]
|
||||
onSelect: (payload: PeriodParamsWithTimeRange) => void
|
||||
}
|
||||
|
||||
|
|
@ -41,13 +45,13 @@ const RangeSelector: FC<Props> = ({
|
|||
const renderTrigger = useCallback((item: Item | null, isOpen: boolean) => {
|
||||
return (
|
||||
<div className={cn('flex h-8 cursor-pointer items-center space-x-1.5 rounded-lg bg-components-input-bg-normal pl-3 pr-2', isOpen && 'bg-state-base-hover-alt')}>
|
||||
<div className='system-sm-regular text-components-input-text-filled'>{isCustomRange ? t('appLog.filter.period.custom') : item?.name}</div>
|
||||
<div className="system-sm-regular text-components-input-text-filled">{isCustomRange ? t('filter.period.custom', { ns: 'appLog' }) : item?.name}</div>
|
||||
<RiArrowDownSLine className={cn('size-4 text-text-quaternary', isOpen && 'text-text-secondary')} />
|
||||
</div>
|
||||
)
|
||||
}, [isCustomRange])
|
||||
|
||||
const renderOption = useCallback(({ item, selected }: { item: Item; selected: boolean }) => {
|
||||
const renderOption = useCallback(({ item, selected }: { item: Item, selected: boolean }) => {
|
||||
return (
|
||||
<>
|
||||
{selected && (
|
||||
|
|
@ -65,15 +69,15 @@ const RangeSelector: FC<Props> = ({
|
|||
}, [])
|
||||
return (
|
||||
<SimpleSelect
|
||||
items={ranges.map(v => ({ ...v, name: t(`appLog.filter.period.${v.name}`) }))}
|
||||
className='mt-0 !w-40'
|
||||
items={ranges.map(v => ({ ...v, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))}
|
||||
className="mt-0 !w-40"
|
||||
notClearable={true}
|
||||
onSelect={handleSelectRange}
|
||||
defaultValue={0}
|
||||
wrapperClassName='h-8'
|
||||
optionWrapClassName='w-[200px] translate-x-[-24px]'
|
||||
wrapperClassName="h-8"
|
||||
optionWrapClassName="w-[200px] translate-x-[-24px]"
|
||||
renderTrigger={renderTrigger}
|
||||
optionClassName='flex items-center py-0 pl-7 pr-2 h-8'
|
||||
optionClassName="flex items-center py-0 pl-7 pr-2 h-8"
|
||||
renderOption={renderOption}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import * as React from 'react'
|
||||
import { OpikIconBig } from '@/app/components/base/icons/src/public/tracing'
|
||||
import iconData from '@/app/components/base/icons/src/public/tracing/OpikIconBig.json'
|
||||
import { normalizeAttrs } from '@/app/components/base/icons/utils'
|
||||
|
||||
describe('SVG Attribute Error Reproduction', () => {
|
||||
// Capture console errors
|
||||
|
|
@ -10,7 +11,7 @@ describe('SVG Attribute Error Reproduction', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
errorMessages = []
|
||||
console.error = jest.fn((message) => {
|
||||
console.error = vi.fn((message) => {
|
||||
errorMessages.push(message)
|
||||
originalError(message)
|
||||
})
|
||||
|
|
@ -54,9 +55,6 @@ describe('SVG Attribute Error Reproduction', () => {
|
|||
it('should analyze the SVG structure causing the errors', () => {
|
||||
console.log('\n=== ANALYZING SVG STRUCTURE ===')
|
||||
|
||||
// Import the JSON data directly
|
||||
const iconData = require('@/app/components/base/icons/src/public/tracing/OpikIconBig.json')
|
||||
|
||||
console.log('Icon structure analysis:')
|
||||
console.log('- Root element:', iconData.icon.name)
|
||||
console.log('- Children count:', iconData.icon.children?.length || 0)
|
||||
|
|
@ -113,8 +111,6 @@ describe('SVG Attribute Error Reproduction', () => {
|
|||
it('should test the normalizeAttrs function behavior', () => {
|
||||
console.log('\n=== TESTING normalizeAttrs FUNCTION ===')
|
||||
|
||||
const { normalizeAttrs } = require('@/app/components/base/icons/utils')
|
||||
|
||||
const testAttributes = {
|
||||
'inkscape:showpageshadow': '2',
|
||||
'inkscape:pageopacity': '0.0',
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
|
||||
import type { PopupProps } from './config-popup'
|
||||
import ConfigPopup from './config-popup'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ConfigPopup from './config-popup'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
|
|
@ -42,7 +43,7 @@ const ConfigBtn: FC<Props> = ({
|
|||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
placement="bottom-end"
|
||||
offset={12}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||
|
|
@ -50,7 +51,7 @@ const ConfigBtn: FC<Props> = ({
|
|||
{children}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[11]'>
|
||||
<PortalToFollowElemContent className="z-[11]">
|
||||
<ConfigPopup {...popupProps} />
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
'use client'
|
||||
import type { FC, JSX } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import TracingIcon from './tracing-icon'
|
||||
import ProviderPanel from './provider-panel'
|
||||
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
|
||||
import { TracingProvider } from './type'
|
||||
import ProviderConfigModal from './provider-config-modal'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import cn from '@/utils/classnames'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ProviderConfigModal from './provider-config-modal'
|
||||
import ProviderPanel from './provider-panel'
|
||||
import TracingIcon from './tracing-icon'
|
||||
import { TracingProvider } from './type'
|
||||
|
||||
const I18N_PREFIX = 'app.tracing'
|
||||
const I18N_PREFIX = 'tracing'
|
||||
|
||||
export type PopupProps = {
|
||||
appId: string
|
||||
|
|
@ -92,7 +93,7 @@ const ConfigPopup: FC<PopupProps> = ({
|
|||
|
||||
const switchContent = (
|
||||
<Switch
|
||||
className='ml-3'
|
||||
className="ml-3"
|
||||
defaultValue={enabled}
|
||||
onChange={onStatusChange}
|
||||
disabled={providerAllNotConfigured}
|
||||
|
|
@ -322,68 +323,68 @@ const ConfigPopup: FC<PopupProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='w-[420px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-4 shadow-xl'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<TracingIcon size='md' className='mr-2' />
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t(`${I18N_PREFIX}.tracing`)}</div>
|
||||
<div className="w-[420px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-4 shadow-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<TracingIcon size="md" className="mr-2" />
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t(`${I18N_PREFIX}.tracing`, { ns: 'app' })}</div>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<div className="flex items-center">
|
||||
<Indicator color={enabled ? 'green' : 'gray'} />
|
||||
<div className={cn('system-xs-semibold-uppercase ml-1 text-text-tertiary', enabled && 'text-util-colors-green-green-600')}>
|
||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
|
||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<>
|
||||
{providerAllNotConfigured
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={t(`${I18N_PREFIX}.disabledTip`)}
|
||||
>
|
||||
{switchContent}
|
||||
</Tooltip>
|
||||
)
|
||||
<Tooltip
|
||||
popupContent={t(`${I18N_PREFIX}.disabledTip`, { ns: 'app' })}
|
||||
>
|
||||
{switchContent}
|
||||
</Tooltip>
|
||||
)
|
||||
: switchContent}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='system-xs-regular mt-2 text-text-tertiary'>
|
||||
{t(`${I18N_PREFIX}.tracingDescription`)}
|
||||
<div className="system-xs-regular mt-2 text-text-tertiary">
|
||||
{t(`${I18N_PREFIX}.tracingDescription`, { ns: 'app' })}
|
||||
</div>
|
||||
<Divider className='my-3' />
|
||||
<div className='relative'>
|
||||
<Divider className="my-3" />
|
||||
<div className="relative">
|
||||
{(providerAllConfigured || providerAllNotConfigured)
|
||||
? (
|
||||
<>
|
||||
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div>
|
||||
<div className='mt-2 max-h-96 space-y-2 overflow-y-auto'>
|
||||
{langfusePanel}
|
||||
{langSmithPanel}
|
||||
{opikPanel}
|
||||
{mlflowPanel}
|
||||
{databricksPanel}
|
||||
{weavePanel}
|
||||
{arizePanel}
|
||||
{phoenixPanel}
|
||||
{aliyunPanel}
|
||||
{tencentPanel}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
<>
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`, { ns: 'app' })}</div>
|
||||
<div className="mt-2 max-h-96 space-y-2 overflow-y-auto">
|
||||
{langfusePanel}
|
||||
{langSmithPanel}
|
||||
{opikPanel}
|
||||
{mlflowPanel}
|
||||
{databricksPanel}
|
||||
{weavePanel}
|
||||
{arizePanel}
|
||||
{phoenixPanel}
|
||||
{aliyunPanel}
|
||||
{tencentPanel}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</div>
|
||||
<div className='mt-2 max-h-40 space-y-2 overflow-y-auto'>
|
||||
{configuredProviderPanel()}
|
||||
</div>
|
||||
<div className='system-xs-medium-uppercase mt-3 text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
|
||||
<div className='mt-2 max-h-40 space-y-2 overflow-y-auto'>
|
||||
{moreProviderPanel()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.configured`, { ns: 'app' })}</div>
|
||||
<div className="mt-2 max-h-40 space-y-2 overflow-y-auto">
|
||||
{configuredProviderPanel()}
|
||||
</div>
|
||||
<div className="system-xs-medium-uppercase mt-3 text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`, { ns: 'app' })}</div>
|
||||
<div className="mt-2 max-h-40 space-y-2 overflow-y-auto">
|
||||
{moreProviderPanel()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
{isShowConfigModal && (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import * as React from 'react'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
|
|
@ -25,14 +25,17 @@ const Field: FC<Props> = ({
|
|||
}) => {
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<div className='flex py-[7px]'>
|
||||
<div className={cn(labelClassName, 'flex h-[18px] items-center text-[13px] font-medium text-text-primary')}>{label} </div>
|
||||
{isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
|
||||
<div className="flex py-[7px]">
|
||||
<div className={cn(labelClassName, 'flex h-[18px] items-center text-[13px] font-medium text-text-primary')}>
|
||||
{label}
|
||||
{' '}
|
||||
</div>
|
||||
{isRequired && <span className="ml-0.5 text-xs font-semibold text-[#D92D20]">*</span>}
|
||||
</div>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
className='h-9'
|
||||
className="h-9"
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
|
||||
import type { TracingStatus } from '@/models/app'
|
||||
import {
|
||||
RiArrowDownDoubleLine,
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
|
||||
import { TracingProvider } from './type'
|
||||
import TracingIcon from './tracing-icon'
|
||||
import ConfigButton from './config-button'
|
||||
import cn from '@/utils/classnames'
|
||||
import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
|
||||
import type { TracingStatus } from '@/models/app'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ConfigButton from './config-button'
|
||||
import TracingIcon from './tracing-icon'
|
||||
import { TracingProvider } from './type'
|
||||
|
||||
const I18N_PREFIX = 'app.tracing'
|
||||
const I18N_PREFIX = 'tracing'
|
||||
|
||||
const Panel: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -44,7 +45,7 @@ const Panel: FC = () => {
|
|||
if (!noToast) {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.success'),
|
||||
message: t('api.success', { ns: 'common' }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -215,8 +216,8 @@ const Panel: FC = () => {
|
|||
|
||||
if (!isLoaded) {
|
||||
return (
|
||||
<div className='mb-3 flex items-center justify-between'>
|
||||
<div className='w-[200px]'>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="w-[200px]">
|
||||
<Loading />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -252,14 +253,14 @@ const Panel: FC = () => {
|
|||
'flex cursor-pointer select-none items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter',
|
||||
)}
|
||||
>
|
||||
<TracingIcon size='md' />
|
||||
<div className='system-sm-semibold mx-2 text-text-secondary'>{t(`${I18N_PREFIX}.title`)}</div>
|
||||
<div className='rounded-md p-1'>
|
||||
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
|
||||
<TracingIcon size="md" />
|
||||
<div className="system-sm-semibold mx-2 text-text-secondary">{t(`${I18N_PREFIX}.title`, { ns: 'app' })}</div>
|
||||
<div className="rounded-md p-1">
|
||||
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<Divider type='vertical' className='h-3.5' />
|
||||
<div className='rounded-md p-1'>
|
||||
<RiArrowDownDoubleLine className='h-4 w-4 text-text-tertiary' />
|
||||
<Divider type="vertical" className="h-3.5" />
|
||||
<div className="rounded-md p-1">
|
||||
<RiArrowDownDoubleLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
</ConfigButton>
|
||||
|
|
@ -291,17 +292,17 @@ const Panel: FC = () => {
|
|||
'flex cursor-pointer select-none items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter',
|
||||
)}
|
||||
>
|
||||
<div className='ml-4 mr-1 flex items-center'>
|
||||
<div className="ml-4 mr-1 flex items-center">
|
||||
<Indicator color={enabled ? 'green' : 'gray'} />
|
||||
<div className='system-xs-semibold-uppercase ml-1.5 text-text-tertiary'>
|
||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
|
||||
<div className="system-xs-semibold-uppercase ml-1.5 text-text-tertiary">
|
||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
|
||||
</div>
|
||||
</div>
|
||||
{InUseProviderIcon && <InUseProviderIcon className='ml-1 h-4' />}
|
||||
<div className='ml-2 rounded-md p-1'>
|
||||
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
|
||||
{InUseProviderIcon && <InUseProviderIcon className="ml-1 h-4" />}
|
||||
<div className="ml-2 rounded-md p-1">
|
||||
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<Divider type='vertical' className='h-3.5' />
|
||||
<Divider type="vertical" className="h-3.5" />
|
||||
</div>
|
||||
</ConfigButton>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import Field from './field'
|
||||
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
|
||||
import { TracingProvider } from './type'
|
||||
import { docURL } from './config'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
|
||||
import { docURL } from './config'
|
||||
import Field from './field'
|
||||
import { TracingProvider } from './type'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
|
|
@ -29,7 +30,7 @@ type Props = {
|
|||
onChosen: (provider: TracingProvider) => void
|
||||
}
|
||||
|
||||
const I18N_PREFIX = 'app.tracing.configProvider'
|
||||
const I18N_PREFIX = 'tracing.configProvider'
|
||||
|
||||
const arizeConfigTemplate = {
|
||||
api_key: '',
|
||||
|
|
@ -156,7 +157,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
})
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.remove'),
|
||||
message: t('api.remove', { ns: 'common' }),
|
||||
})
|
||||
onRemoved()
|
||||
hideRemoveConfirm()
|
||||
|
|
@ -176,37 +177,37 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
if (type === TracingProvider.arize) {
|
||||
const postData = config as ArizeConfig
|
||||
if (!postData.api_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'API Key' })
|
||||
if (!postData.space_id)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Space ID' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Space ID' })
|
||||
if (!errorMessage && !postData.project)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.phoenix) {
|
||||
const postData = config as PhoenixConfig
|
||||
if (!postData.api_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'API Key' })
|
||||
if (!errorMessage && !postData.project)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.langSmith) {
|
||||
const postData = config as LangSmithConfig
|
||||
if (!postData.api_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'API Key' })
|
||||
if (!errorMessage && !postData.project)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.langfuse) {
|
||||
const postData = config as LangFuseConfig
|
||||
if (!errorMessage && !postData.secret_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.secretKey`) })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.secretKey`, { ns: 'app' }) })
|
||||
if (!errorMessage && !postData.public_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.publicKey`) })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.publicKey`, { ns: 'app' }) })
|
||||
if (!errorMessage && !postData.host)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Host' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Host' })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.opik) {
|
||||
|
|
@ -217,43 +218,43 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
if (type === TracingProvider.weave) {
|
||||
const postData = config as WeaveConfig
|
||||
if (!errorMessage && !postData.api_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'API Key' })
|
||||
if (!errorMessage && !postData.project)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.aliyun) {
|
||||
const postData = config as AliyunConfig
|
||||
if (!errorMessage && !postData.app_name)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'App Name' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'App Name' })
|
||||
if (!errorMessage && !postData.license_key)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'License Key' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'License Key' })
|
||||
if (!errorMessage && !postData.endpoint)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Endpoint' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Endpoint' })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.mlflow) {
|
||||
const postData = config as MLflowConfig
|
||||
if (!errorMessage && !postData.tracking_uri)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Tracking URI' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Tracking URI' })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.databricks) {
|
||||
const postData = config as DatabricksConfig
|
||||
if (!errorMessage && !postData.experiment_id)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Experiment ID' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Experiment ID' })
|
||||
if (!errorMessage && !postData.host)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Host' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Host' })
|
||||
}
|
||||
|
||||
if (type === TracingProvider.tencent) {
|
||||
const postData = config as TencentConfig
|
||||
if (!errorMessage && !postData.token)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Token' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Token' })
|
||||
if (!errorMessage && !postData.endpoint)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Endpoint' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Endpoint' })
|
||||
if (!errorMessage && !postData.service_name)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Service Name' })
|
||||
errorMessage = t('errorMsg.fieldRequired', { ns: 'common', field: 'Service Name' })
|
||||
}
|
||||
|
||||
return errorMessage
|
||||
|
|
@ -280,7 +281,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
})
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.success'),
|
||||
message: t('api.success', { ns: 'common' }),
|
||||
})
|
||||
onSaved(config)
|
||||
if (isAdd)
|
||||
|
|
@ -295,403 +296,407 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
<>
|
||||
{!isShowRemoveConfirm
|
||||
? (
|
||||
<PortalToFollowElem open>
|
||||
<PortalToFollowElemContent className='z-[60] h-full w-full'>
|
||||
<div className='fixed inset-0 flex items-center justify-center bg-background-overlay'>
|
||||
<div className='mx-2 max-h-[calc(100vh-120px)] w-[640px] overflow-y-auto rounded-2xl bg-components-panel-bg shadow-xl'>
|
||||
<div className='px-8 pt-8'>
|
||||
<div className='mb-4 flex items-center justify-between'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t(`${I18N_PREFIX}.title`)}{t(`app.tracing.${type}.title`)}</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
{type === TracingProvider.arize && (
|
||||
<>
|
||||
<Field
|
||||
label='API Key'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as ArizeConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label='Space ID'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as ArizeConfig).space_id}
|
||||
onChange={handleConfigChange('space_id')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Space ID' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`)!}
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as ArizeConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
|
||||
/>
|
||||
<Field
|
||||
label='Endpoint'
|
||||
labelClassName='!text-sm'
|
||||
value={(config as ArizeConfig).endpoint}
|
||||
onChange={handleConfigChange('endpoint')}
|
||||
placeholder={'https://otlp.arize.com'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.phoenix && (
|
||||
<>
|
||||
<Field
|
||||
label='API Key'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as PhoenixConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`)!}
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as PhoenixConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
|
||||
/>
|
||||
<Field
|
||||
label='Endpoint'
|
||||
labelClassName='!text-sm'
|
||||
value={(config as PhoenixConfig).endpoint}
|
||||
onChange={handleConfigChange('endpoint')}
|
||||
placeholder={'https://app.phoenix.arize.com'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.aliyun && (
|
||||
<>
|
||||
<Field
|
||||
label='License Key'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as AliyunConfig).license_key}
|
||||
onChange={handleConfigChange('license_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'License Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label='Endpoint'
|
||||
labelClassName='!text-sm'
|
||||
value={(config as AliyunConfig).endpoint}
|
||||
onChange={handleConfigChange('endpoint')}
|
||||
placeholder={'https://tracing.arms.aliyuncs.com'}
|
||||
/>
|
||||
<Field
|
||||
label='App Name'
|
||||
labelClassName='!text-sm'
|
||||
value={(config as AliyunConfig).app_name}
|
||||
onChange={handleConfigChange('app_name')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.tencent && (
|
||||
<>
|
||||
<Field
|
||||
label='Token'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as TencentConfig).token}
|
||||
onChange={handleConfigChange('token')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Token' })!}
|
||||
/>
|
||||
<Field
|
||||
label='Endpoint'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as TencentConfig).endpoint}
|
||||
onChange={handleConfigChange('endpoint')}
|
||||
placeholder='https://your-region.cls.tencentcs.com'
|
||||
/>
|
||||
<Field
|
||||
label='Service Name'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as TencentConfig).service_name}
|
||||
onChange={handleConfigChange('service_name')}
|
||||
placeholder='dify_app'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.weave && (
|
||||
<>
|
||||
<Field
|
||||
label='API Key'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as WeaveConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`)!}
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as WeaveConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
|
||||
/>
|
||||
<Field
|
||||
label='Entity'
|
||||
labelClassName='!text-sm'
|
||||
value={(config as WeaveConfig).entity}
|
||||
onChange={handleConfigChange('entity')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Entity' })!}
|
||||
/>
|
||||
<Field
|
||||
label='Endpoint'
|
||||
labelClassName='!text-sm'
|
||||
value={(config as WeaveConfig).endpoint}
|
||||
onChange={handleConfigChange('endpoint')}
|
||||
placeholder={'https://trace.wandb.ai/'}
|
||||
/>
|
||||
<Field
|
||||
label='Host'
|
||||
labelClassName='!text-sm'
|
||||
value={(config as WeaveConfig).host}
|
||||
onChange={handleConfigChange('host')}
|
||||
placeholder={'https://api.wandb.ai'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.langSmith && (
|
||||
<>
|
||||
<Field
|
||||
label='API Key'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as LangSmithConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`)!}
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as LangSmithConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
|
||||
/>
|
||||
<Field
|
||||
label='Endpoint'
|
||||
labelClassName='!text-sm'
|
||||
value={(config as LangSmithConfig).endpoint}
|
||||
onChange={handleConfigChange('endpoint')}
|
||||
placeholder={'https://api.smith.langchain.com'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.langfuse && (
|
||||
<>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.secretKey`)!}
|
||||
labelClassName='!text-sm'
|
||||
value={(config as LangFuseConfig).secret_key}
|
||||
isRequired
|
||||
onChange={handleConfigChange('secret_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.secretKey`) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.publicKey`)!}
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as LangFuseConfig).public_key}
|
||||
onChange={handleConfigChange('public_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.publicKey`) })!}
|
||||
/>
|
||||
<Field
|
||||
label='Host'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as LangFuseConfig).host}
|
||||
onChange={handleConfigChange('host')}
|
||||
placeholder='https://cloud.langfuse.com'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.opik && (
|
||||
<>
|
||||
<Field
|
||||
label='API Key'
|
||||
labelClassName='!text-sm'
|
||||
value={(config as OpikConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`)!}
|
||||
labelClassName='!text-sm'
|
||||
value={(config as OpikConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
|
||||
/>
|
||||
<Field
|
||||
label='Workspace'
|
||||
labelClassName='!text-sm'
|
||||
value={(config as OpikConfig).workspace}
|
||||
onChange={handleConfigChange('workspace')}
|
||||
placeholder={'default'}
|
||||
/>
|
||||
<Field
|
||||
label='Url'
|
||||
labelClassName='!text-sm'
|
||||
value={(config as OpikConfig).url}
|
||||
onChange={handleConfigChange('url')}
|
||||
placeholder={'https://www.comet.com/opik/api/'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.mlflow && (
|
||||
<>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.trackingUri`)!}
|
||||
labelClassName='!text-sm'
|
||||
value={(config as MLflowConfig).tracking_uri}
|
||||
isRequired
|
||||
onChange={handleConfigChange('tracking_uri')}
|
||||
placeholder={'http://localhost:5000'}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.experimentId`)!}
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={(config as MLflowConfig).experiment_id}
|
||||
onChange={handleConfigChange('experiment_id')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.experimentId`) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.username`)!}
|
||||
labelClassName='!text-sm'
|
||||
value={(config as MLflowConfig).username}
|
||||
onChange={handleConfigChange('username')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.username`) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.password`)!}
|
||||
labelClassName='!text-sm'
|
||||
value={(config as MLflowConfig).password}
|
||||
onChange={handleConfigChange('password')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.password`) })!}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.databricks && (
|
||||
<>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.experimentId`)!}
|
||||
labelClassName='!text-sm'
|
||||
value={(config as DatabricksConfig).experiment_id}
|
||||
onChange={handleConfigChange('experiment_id')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.experimentId`) })!}
|
||||
isRequired
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.databricksHost`)!}
|
||||
labelClassName='!text-sm'
|
||||
value={(config as DatabricksConfig).host}
|
||||
onChange={handleConfigChange('host')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.databricksHost`) })!}
|
||||
isRequired
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.clientId`)!}
|
||||
labelClassName='!text-sm'
|
||||
value={(config as DatabricksConfig).client_id}
|
||||
onChange={handleConfigChange('client_id')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientId`) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.clientSecret`)!}
|
||||
labelClassName='!text-sm'
|
||||
value={(config as DatabricksConfig).client_secret}
|
||||
onChange={handleConfigChange('client_secret')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientSecret`) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.personalAccessToken`)!}
|
||||
labelClassName='!text-sm'
|
||||
value={(config as DatabricksConfig).personal_access_token}
|
||||
onChange={handleConfigChange('personal_access_token')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.personalAccessToken`) })!}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='my-8 flex h-8 items-center justify-between'>
|
||||
<a
|
||||
className='flex items-center space-x-1 text-xs font-normal leading-[18px] text-[#155EEF]'
|
||||
target='_blank'
|
||||
href={docURL[type]}
|
||||
>
|
||||
<span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span>
|
||||
<LinkExternal02 className='h-3 w-3' />
|
||||
</a>
|
||||
<div className='flex items-center'>
|
||||
{isEdit && (
|
||||
<>
|
||||
<Button
|
||||
className='h-9 text-sm font-medium text-text-secondary'
|
||||
onClick={showRemoveConfirm}
|
||||
>
|
||||
<span className='text-[#D92D20]'>{t('common.operation.remove')}</span>
|
||||
</Button>
|
||||
<Divider type='vertical' className='mx-3 h-[18px]' />
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
className='mr-2 h-9 text-sm font-medium text-text-secondary'
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className='h-9 text-sm font-medium'
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
>
|
||||
{t(`common.operation.${isAdd ? 'saveAndEnable' : 'save'}`)}
|
||||
</Button>
|
||||
<PortalToFollowElem open>
|
||||
<PortalToFollowElemContent className="z-[60] h-full w-full">
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-background-overlay">
|
||||
<div className="mx-2 max-h-[calc(100vh-120px)] w-[640px] overflow-y-auto rounded-2xl bg-components-panel-bg shadow-xl">
|
||||
<div className="px-8 pt-8">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="title-2xl-semi-bold text-text-primary">
|
||||
{t(`${I18N_PREFIX}.title`, { ns: 'app' })}
|
||||
{t(`tracing.${type}.title`, { ns: 'app' })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{type === TracingProvider.arize && (
|
||||
<>
|
||||
<Field
|
||||
label="API Key"
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as ArizeConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label="Space ID"
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as ArizeConfig).space_id}
|
||||
onChange={handleConfigChange('space_id')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'Space ID' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as ArizeConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label="Endpoint"
|
||||
labelClassName="!text-sm"
|
||||
value={(config as ArizeConfig).endpoint}
|
||||
onChange={handleConfigChange('endpoint')}
|
||||
placeholder="https://otlp.arize.com"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.phoenix && (
|
||||
<>
|
||||
<Field
|
||||
label="API Key"
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as PhoenixConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as PhoenixConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label="Endpoint"
|
||||
labelClassName="!text-sm"
|
||||
value={(config as PhoenixConfig).endpoint}
|
||||
onChange={handleConfigChange('endpoint')}
|
||||
placeholder="https://app.phoenix.arize.com"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.aliyun && (
|
||||
<>
|
||||
<Field
|
||||
label="License Key"
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as AliyunConfig).license_key}
|
||||
onChange={handleConfigChange('license_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'License Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label="Endpoint"
|
||||
labelClassName="!text-sm"
|
||||
value={(config as AliyunConfig).endpoint}
|
||||
onChange={handleConfigChange('endpoint')}
|
||||
placeholder="https://tracing.arms.aliyuncs.com"
|
||||
/>
|
||||
<Field
|
||||
label="App Name"
|
||||
labelClassName="!text-sm"
|
||||
value={(config as AliyunConfig).app_name}
|
||||
onChange={handleConfigChange('app_name')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.tencent && (
|
||||
<>
|
||||
<Field
|
||||
label="Token"
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as TencentConfig).token}
|
||||
onChange={handleConfigChange('token')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'Token' })!}
|
||||
/>
|
||||
<Field
|
||||
label="Endpoint"
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as TencentConfig).endpoint}
|
||||
onChange={handleConfigChange('endpoint')}
|
||||
placeholder="https://your-region.cls.tencentcs.com"
|
||||
/>
|
||||
<Field
|
||||
label="Service Name"
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as TencentConfig).service_name}
|
||||
onChange={handleConfigChange('service_name')}
|
||||
placeholder="dify_app"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.weave && (
|
||||
<>
|
||||
<Field
|
||||
label="API Key"
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as WeaveConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as WeaveConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label="Entity"
|
||||
labelClassName="!text-sm"
|
||||
value={(config as WeaveConfig).entity}
|
||||
onChange={handleConfigChange('entity')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'Entity' })!}
|
||||
/>
|
||||
<Field
|
||||
label="Endpoint"
|
||||
labelClassName="!text-sm"
|
||||
value={(config as WeaveConfig).endpoint}
|
||||
onChange={handleConfigChange('endpoint')}
|
||||
placeholder="https://trace.wandb.ai/"
|
||||
/>
|
||||
<Field
|
||||
label="Host"
|
||||
labelClassName="!text-sm"
|
||||
value={(config as WeaveConfig).host}
|
||||
onChange={handleConfigChange('host')}
|
||||
placeholder="https://api.wandb.ai"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.langSmith && (
|
||||
<>
|
||||
<Field
|
||||
label="API Key"
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as LangSmithConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as LangSmithConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label="Endpoint"
|
||||
labelClassName="!text-sm"
|
||||
value={(config as LangSmithConfig).endpoint}
|
||||
onChange={handleConfigChange('endpoint')}
|
||||
placeholder="https://api.smith.langchain.com"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.langfuse && (
|
||||
<>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.secretKey`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as LangFuseConfig).secret_key}
|
||||
isRequired
|
||||
onChange={handleConfigChange('secret_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.secretKey`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.publicKey`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as LangFuseConfig).public_key}
|
||||
onChange={handleConfigChange('public_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.publicKey`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label="Host"
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as LangFuseConfig).host}
|
||||
onChange={handleConfigChange('host')}
|
||||
placeholder="https://cloud.langfuse.com"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.opik && (
|
||||
<>
|
||||
<Field
|
||||
label="API Key"
|
||||
labelClassName="!text-sm"
|
||||
value={(config as OpikConfig).api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: 'API Key' })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.project`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as OpikConfig).project}
|
||||
onChange={handleConfigChange('project')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.project`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label="Workspace"
|
||||
labelClassName="!text-sm"
|
||||
value={(config as OpikConfig).workspace}
|
||||
onChange={handleConfigChange('workspace')}
|
||||
placeholder="default"
|
||||
/>
|
||||
<Field
|
||||
label="Url"
|
||||
labelClassName="!text-sm"
|
||||
value={(config as OpikConfig).url}
|
||||
onChange={handleConfigChange('url')}
|
||||
placeholder="https://www.comet.com/opik/api/"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.mlflow && (
|
||||
<>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.trackingUri`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as MLflowConfig).tracking_uri}
|
||||
isRequired
|
||||
onChange={handleConfigChange('tracking_uri')}
|
||||
placeholder="http://localhost:5000"
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.experimentId`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
isRequired
|
||||
value={(config as MLflowConfig).experiment_id}
|
||||
onChange={handleConfigChange('experiment_id')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.experimentId`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.username`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as MLflowConfig).username}
|
||||
onChange={handleConfigChange('username')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.username`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.password`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as MLflowConfig).password}
|
||||
onChange={handleConfigChange('password')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.password`, { ns: 'app' }) })!}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === TracingProvider.databricks && (
|
||||
<>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.experimentId`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as DatabricksConfig).experiment_id}
|
||||
onChange={handleConfigChange('experiment_id')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.experimentId`, { ns: 'app' }) })!}
|
||||
isRequired
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.databricksHost`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as DatabricksConfig).host}
|
||||
onChange={handleConfigChange('host')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.databricksHost`, { ns: 'app' }) })!}
|
||||
isRequired
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.clientId`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as DatabricksConfig).client_id}
|
||||
onChange={handleConfigChange('client_id')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.clientId`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.clientSecret`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as DatabricksConfig).client_secret}
|
||||
onChange={handleConfigChange('client_secret')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.clientSecret`, { ns: 'app' }) })!}
|
||||
/>
|
||||
<Field
|
||||
label={t(`${I18N_PREFIX}.personalAccessToken`, { ns: 'app' })!}
|
||||
labelClassName="!text-sm"
|
||||
value={(config as DatabricksConfig).personal_access_token}
|
||||
onChange={handleConfigChange('personal_access_token')}
|
||||
placeholder={t(`${I18N_PREFIX}.placeholder`, { ns: 'app', key: t(`${I18N_PREFIX}.personalAccessToken`, { ns: 'app' }) })!}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="my-8 flex h-8 items-center justify-between">
|
||||
<a
|
||||
className="flex items-center space-x-1 text-xs font-normal leading-[18px] text-[#155EEF]"
|
||||
target="_blank"
|
||||
href={docURL[type]}
|
||||
>
|
||||
<span>{t(`${I18N_PREFIX}.viewDocsLink`, { ns: 'app', key: t(`tracing.${type}.title`, { ns: 'app' }) })}</span>
|
||||
<LinkExternal02 className="h-3 w-3" />
|
||||
</a>
|
||||
<div className="flex items-center">
|
||||
{isEdit && (
|
||||
<>
|
||||
<Button
|
||||
className="h-9 text-sm font-medium text-text-secondary"
|
||||
onClick={showRemoveConfirm}
|
||||
>
|
||||
<span className="text-[#D92D20]">{t('operation.remove', { ns: 'common' })}</span>
|
||||
</Button>
|
||||
<Divider type="vertical" className="mx-3 h-[18px]" />
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
className="mr-2 h-9 text-sm font-medium text-text-secondary"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="h-9 text-sm font-medium"
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
>
|
||||
{t(`operation.${isAdd ? 'saveAndEnable' : 'save'}`, { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-t-[0.5px] border-divider-regular'>
|
||||
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
||||
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='mx-1 text-primary-600'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
<div className="border-t-[0.5px] border-divider-regular">
|
||||
<div className="flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary">
|
||||
<Lock01 className="mr-1 h-3 w-3 text-text-tertiary" />
|
||||
{t('modelProvider.encrypted.front', { ns: 'common' })}
|
||||
<a
|
||||
className="mx-1 text-primary-600"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html"
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('modelProvider.encrypted.back', { ns: 'common' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
: (
|
||||
<Confirm
|
||||
isShow
|
||||
type='warning'
|
||||
title={t(`${I18N_PREFIX}.removeConfirmTitle`, { key: t(`app.tracing.${type}.title`) })!}
|
||||
content={t(`${I18N_PREFIX}.removeConfirmContent`)}
|
||||
onConfirm={handleRemove}
|
||||
onCancel={hideRemoveConfirm}
|
||||
/>
|
||||
)}
|
||||
<Confirm
|
||||
isShow
|
||||
type="warning"
|
||||
title={t(`${I18N_PREFIX}.removeConfirmTitle`, { ns: 'app', key: t(`tracing.${type}.title`, { ns: 'app' }) })!}
|
||||
content={t(`${I18N_PREFIX}.removeConfirmContent`, { ns: 'app' })}
|
||||
onConfirm={handleRemove}
|
||||
onCancel={hideRemoveConfirm}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import {
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TracingProvider } from './type'
|
||||
import cn from '@/utils/classnames'
|
||||
import { AliyunIconBig, ArizeIconBig, DatabricksIconBig, LangfuseIconBig, LangsmithIconBig, MlflowIconBig, OpikIconBig, PhoenixIconBig, TencentIconBig, WeaveIconBig } from '@/app/components/base/icons/src/public/tracing'
|
||||
import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { TracingProvider } from './type'
|
||||
|
||||
const I18N_PREFIX = 'app.tracing'
|
||||
const I18N_PREFIX = 'tracing'
|
||||
|
||||
type Props = {
|
||||
type: TracingProvider
|
||||
|
|
@ -78,31 +79,31 @@ const ProviderPanel: FC<Props> = ({
|
|||
)}
|
||||
onClick={handleChosen}
|
||||
>
|
||||
<div className={'flex items-center justify-between space-x-1'}>
|
||||
<div className='flex items-center'>
|
||||
<Icon className='h-6' />
|
||||
{isChosen && <div className='system-2xs-medium-uppercase ml-1 flex h-4 items-center rounded-[4px] border border-text-accent-secondary px-1 text-text-accent-secondary'>{t(`${I18N_PREFIX}.inUse`)}</div>}
|
||||
<div className="flex items-center justify-between space-x-1">
|
||||
<div className="flex items-center">
|
||||
<Icon className="h-6" />
|
||||
{isChosen && <div className="system-2xs-medium-uppercase ml-1 flex h-4 items-center rounded-[4px] border border-text-accent-secondary px-1 text-text-accent-secondary">{t(`${I18N_PREFIX}.inUse`, { ns: 'app' })}</div>}
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div className={'flex items-center justify-between space-x-1'}>
|
||||
<div className="flex items-center justify-between space-x-1">
|
||||
{hasConfigured && (
|
||||
<div className='flex h-6 cursor-pointer items-center space-x-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-text-secondary shadow-xs' onClick={viewBtnClick} >
|
||||
<View className='h-3 w-3' />
|
||||
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.view`)}</div>
|
||||
<div className="flex h-6 cursor-pointer items-center space-x-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-text-secondary shadow-xs" onClick={viewBtnClick}>
|
||||
<View className="h-3 w-3" />
|
||||
<div className="text-xs font-medium">{t(`${I18N_PREFIX}.view`, { ns: 'app' })}</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className='flex h-6 cursor-pointer items-center space-x-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-text-secondary shadow-xs'
|
||||
className="flex h-6 cursor-pointer items-center space-x-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-text-secondary shadow-xs"
|
||||
onClick={handleConfigBtnClick}
|
||||
>
|
||||
<RiEqualizer2Line className='h-3 w-3' />
|
||||
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div>
|
||||
<RiEqualizer2Line className="h-3 w-3" />
|
||||
<div className="text-xs font-medium">{t(`${I18N_PREFIX}.config`, { ns: 'app' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='system-xs-regular mt-2 text-text-tertiary'>
|
||||
{t(`${I18N_PREFIX}.${type}.description`)}
|
||||
<div className="system-xs-regular mt-2 text-text-tertiary">
|
||||
{t(`${I18N_PREFIX}.${type}.description`, { ns: 'app' })}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import * as React from 'react'
|
||||
import { TracingIcon as Icon } from '@/app/components/base/icons/src/public/tracing'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
|
|
@ -21,7 +21,7 @@ const TracingIcon: FC<Props> = ({
|
|||
const sizeClass = sizeClassMap[size]
|
||||
return (
|
||||
<div className={cn(className, sizeClass, 'bg-primary-500 shadow-md')}>
|
||||
<Icon className='h-full w-full' />
|
||||
<Icon className="h-full w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import WorkflowApp from '@/app/components/workflow-app'
|
|||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className='h-full w-full overflow-x-auto'>
|
||||
<div className="h-full w-full overflow-x-auto">
|
||||
<WorkflowApp />
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
|
|
@ -14,7 +15,7 @@ const AppDetail: FC<IAppDetail> = ({ children }) => {
|
|||
const router = useRouter()
|
||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('common.menus.appDetail'))
|
||||
useDocumentTitle(t('menus.appDetail', { ns: 'common' }))
|
||||
|
||||
useEffect(() => {
|
||||
if (isCurrentWorkspaceDatasetOperator)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import MainDetail from '@/app/components/datasets/documents/detail'
|
||||
|
||||
export type IDocumentDetailProps = {
|
||||
params: Promise<{ datasetId: string; documentId: string }>
|
||||
params: Promise<{ datasetId: string, documentId: string }>
|
||||
}
|
||||
|
||||
const DocumentDetail = async (props: IDocumentDetailProps) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import Settings from '@/app/components/datasets/documents/detail/settings'
|
||||
|
||||
export type IProps = {
|
||||
params: Promise<{ datasetId: string; documentId: string }>
|
||||
params: Promise<{ datasetId: string, documentId: string }>
|
||||
}
|
||||
|
||||
const DocumentSettings = async (props: IProps) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import CreateFromPipeline from '@/app/components/datasets/documents/create-from-pipeline'
|
||||
|
||||
const CreateFromPipelinePage = async () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import DatasetUpdateForm from '@/app/components/datasets/create'
|
||||
|
||||
export type IProps = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import Main from '@/app/components/datasets/documents'
|
||||
|
||||
export type IProps = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import Main from '@/app/components/datasets/hit-testing'
|
||||
|
||||
type Props = {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { RemixiconComponentType } from '@remixicon/react'
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
RiEqualizer2Fill,
|
||||
RiEqualizer2Line,
|
||||
|
|
@ -12,18 +9,22 @@ import {
|
|||
RiFocus2Fill,
|
||||
RiFocus2Line,
|
||||
} from '@remixicon/react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vender/pipeline'
|
||||
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import ExtraInfo from '@/app/components/datasets/extra-info'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import cn from '@/utils/classnames'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
|
|
@ -69,14 +70,14 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
const navigation = useMemo(() => {
|
||||
const baseNavigation = [
|
||||
{
|
||||
name: t('common.datasetMenus.hitTesting'),
|
||||
name: t('datasetMenus.hitTesting', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/hitTesting`,
|
||||
icon: RiFocus2Line,
|
||||
selectedIcon: RiFocus2Fill,
|
||||
disabled: isButtonDisabledWithPipeline,
|
||||
},
|
||||
{
|
||||
name: t('common.datasetMenus.settings'),
|
||||
name: t('datasetMenus.settings', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/settings`,
|
||||
icon: RiEqualizer2Line,
|
||||
selectedIcon: RiEqualizer2Fill,
|
||||
|
|
@ -86,14 +87,14 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
|
||||
if (datasetRes?.provider !== 'external') {
|
||||
baseNavigation.unshift({
|
||||
name: t('common.datasetMenus.pipeline'),
|
||||
name: t('datasetMenus.pipeline', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/pipeline`,
|
||||
icon: PipelineLine as RemixiconComponentType,
|
||||
selectedIcon: PipelineFill as RemixiconComponentType,
|
||||
disabled: false,
|
||||
})
|
||||
baseNavigation.unshift({
|
||||
name: t('common.datasetMenus.documents'),
|
||||
name: t('datasetMenus.documents', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/documents`,
|
||||
icon: RiFileTextLine,
|
||||
selectedIcon: RiFileTextFill,
|
||||
|
|
@ -104,7 +105,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
return baseNavigation
|
||||
}, [t, datasetId, isButtonDisabledWithPipeline, datasetRes?.provider])
|
||||
|
||||
useDocumentTitle(datasetRes?.name || t('common.menus.datasets'))
|
||||
useDocumentTitle(datasetRes?.name || t('menus.datasets', { ns: 'common' }))
|
||||
|
||||
const setAppSidebarExpand = useStore(state => state.setAppSidebarExpand)
|
||||
|
||||
|
|
@ -115,7 +116,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
}, [isMobile, setAppSidebarExpand])
|
||||
|
||||
if (!datasetRes && !error)
|
||||
return <Loading type='app' />
|
||||
return <Loading type="app" />
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -128,7 +129,8 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
indexingTechnique: datasetRes?.indexing_technique,
|
||||
dataset: datasetRes,
|
||||
mutateDatasetRes,
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{!hideSideBar && (
|
||||
<AppSideBar
|
||||
navigation={navigation}
|
||||
|
|
@ -137,10 +139,10 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
? mode => <ExtraInfo relatedApps={relatedApps} expand={mode === 'expand'} documentCount={datasetRes?.document_count} />
|
||||
: undefined
|
||||
}
|
||||
iconType='dataset'
|
||||
iconType="dataset"
|
||||
/>
|
||||
)}
|
||||
<div className='grow overflow-hidden bg-background-default-subtle'>{children}</div>
|
||||
<div className="grow overflow-hidden bg-background-default-subtle">{children}</div>
|
||||
</DatasetDetailContext.Provider>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import RagPipeline from '@/app/components/rag-pipeline'
|
|||
|
||||
const PipelinePage = () => {
|
||||
return (
|
||||
<div className='h-full w-full overflow-x-auto'>
|
||||
<div className="h-full w-full overflow-x-auto">
|
||||
<RagPipeline />
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import React from 'react'
|
||||
import { getLocaleOnServer, useTranslation as translate } from '@/i18n-config/server'
|
||||
/* eslint-disable dify-i18n/require-ns-option */
|
||||
import * as React from 'react'
|
||||
import Form from '@/app/components/datasets/settings/form'
|
||||
import { getLocaleOnServer, getTranslation } from '@/i18n-config/server'
|
||||
|
||||
const Settings = async () => {
|
||||
const locale = await getLocaleOnServer()
|
||||
const { t } = await translate(locale, 'dataset-settings')
|
||||
const { t } = await getTranslation(locale, 'dataset-settings')
|
||||
|
||||
return (
|
||||
<div className='h-full overflow-y-auto'>
|
||||
<div className='flex flex-col gap-y-0.5 px-6 pb-2 pt-3'>
|
||||
<div className='system-xl-semibold text-text-primary'>{t('title')}</div>
|
||||
<div className='system-sm-regular text-text-tertiary'>{t('desc')}</div>
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="flex flex-col gap-y-0.5 px-6 pb-2 pt-3">
|
||||
<div className="system-xl-semibold text-text-primary">{t('title')}</div>
|
||||
<div className="system-sm-regular text-text-tertiary">{t('desc')}</div>
|
||||
</div>
|
||||
<Form />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
|
||||
export type IDatasetDetail = {
|
||||
children: React.ReactNode
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import ExternalKnowledgeBaseConnector from '@/app/components/datasets/external-knowledge-base/connector'
|
||||
|
||||
const ExternalKnowledgeBaseCreation = () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import CreateFromPipeline from '@/app/components/datasets/create-from-pipeline'
|
||||
|
||||
const DatasetCreation = async () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import DatasetUpdateForm from '@/app/components/datasets/create'
|
||||
|
||||
const DatasetCreation = async () => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect } from 'react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { ExternalApiPanelProvider } from '@/context/external-api-panel-context'
|
||||
import { ExternalKnowledgeApiProvider } from '@/context/external-knowledge-api-context'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function DatasetsLayout({ children }: { children: React.ReactNode }) {
|
||||
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, currentWorkspace, isLoadingCurrentWorkspace } = useAppContext()
|
||||
|
|
@ -19,7 +19,7 @@ export default function DatasetsLayout({ children }: { children: React.ReactNode
|
|||
}, [isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace, currentWorkspace, router])
|
||||
|
||||
if (isLoadingCurrentWorkspace || !(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator))
|
||||
return <Loading type='app' />
|
||||
return <Loading type="app" />
|
||||
return (
|
||||
<ExternalKnowledgeApiProvider>
|
||||
<ExternalApiPanelProvider>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
'use client'
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import {
|
||||
useRouter,
|
||||
useSearchParams,
|
||||
} from 'next/navigation'
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import EducationApplyPage from '@/app/education-apply/education-apply-page'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import AppList from '@/app/components/explore/app-list'
|
||||
|
||||
const Apps = () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import Main from '@/app/components/explore/installed-app'
|
||||
|
||||
export type IInstalledAppProps = {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
'use client'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ExploreClient from '@/app/components/explore'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
|
||||
const ExploreLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('common.menus.explore'))
|
||||
useDocumentTitle(t('menus.explore', { ns: 'common' }))
|
||||
return (
|
||||
<ExploreClient>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import React from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import SwrInitializer from '@/app/components/swr-initializer'
|
||||
import { AppContextProvider } from '@/context/app-context'
|
||||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
import * as React from 'react'
|
||||
import { AppInitializer } from '@/app/components/app-initializer'
|
||||
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
||||
import Header from '@/app/components/header'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||
import { ProviderContextProvider } from '@/context/provider-context'
|
||||
import { ModalContextProvider } from '@/context/modal-context'
|
||||
import GotoAnything from '@/app/components/goto-anything'
|
||||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
import Zendesk from '@/app/components/base/zendesk'
|
||||
import PartnerStack from '../components/billing/partner-stack'
|
||||
import GotoAnything from '@/app/components/goto-anything'
|
||||
import Header from '@/app/components/header'
|
||||
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
||||
import ReadmePanel from '@/app/components/plugins/readme-panel'
|
||||
import { AppContextProvider } from '@/context/app-context'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||
import { ModalContextProvider } from '@/context/modal-context'
|
||||
import { ProviderContextProvider } from '@/context/provider-context'
|
||||
import PartnerStack from '../components/billing/partner-stack'
|
||||
import Splash from '../components/splash'
|
||||
|
||||
const Layout = ({ children }: { children: ReactNode }) => {
|
||||
|
|
@ -20,7 +20,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
<>
|
||||
<GA gaType={GaType.admin} />
|
||||
<AmplitudeProvider />
|
||||
<SwrInitializer>
|
||||
<AppInitializer>
|
||||
<AppContextProvider>
|
||||
<EventEmitterContextProvider>
|
||||
<ProviderContextProvider>
|
||||
|
|
@ -38,7 +38,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
</EventEmitterContextProvider>
|
||||
</AppContextProvider>
|
||||
<Zendesk />
|
||||
</SwrInitializer>
|
||||
</AppInitializer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Marketplace from '@/app/components/plugins/marketplace'
|
||||
import PluginPage from '@/app/components/plugins/plugin-page'
|
||||
import PluginsPanel from '@/app/components/plugins/plugin-page/plugins-panel'
|
||||
import Marketplace from '@/app/components/plugins/marketplace'
|
||||
import { getLocaleOnServer } from '@/i18n-config/server'
|
||||
|
||||
const PluginList = async () => {
|
||||
|
|
@ -8,7 +8,7 @@ const PluginList = async () => {
|
|||
return (
|
||||
<PluginPage
|
||||
plugins={<PluginsPanel />}
|
||||
marketplace={<Marketplace locale={locale} pluginTypeSwitchClassName='top-[60px]' showSearchParams={false} />}
|
||||
marketplace={<Marketplace locale={locale} pluginTypeSwitchClassName="top-[60px]" showSearchParams={false} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useEffect } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ToolProviderList from '@/app/components/tools/provider-list'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
|
||||
const ToolsList: FC = () => {
|
||||
const router = useRouter()
|
||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('common.menus.tools'))
|
||||
useDocumentTitle(t('menus.tools', { ns: 'common' }))
|
||||
|
||||
useEffect(() => {
|
||||
if (isCurrentWorkspaceDatasetOperator)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history'
|
||||
import AuthenticatedLayout from '../../components/authenticated-layout'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot'
|
||||
import AuthenticatedLayout from '../../components/authenticated-layout'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import Main from '@/app/components/share/text-generation'
|
||||
import AuthenticatedLayout from '../../components/authenticated-layout'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
'use client'
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { useGetWebAppInfo, useGetWebAppMeta, useGetWebAppParams } from '@/service/use-share'
|
||||
import { webAppLogout } from '@/service/webapp-auth'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -49,35 +50,47 @@ const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
|
|||
}, [getSigninUrl, router, webAppLogout, shareCode])
|
||||
|
||||
if (appInfoError) {
|
||||
return <div className='flex h-full items-center justify-center'>
|
||||
<AppUnavailable unknownReason={appInfoError.message} />
|
||||
</div>
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<AppUnavailable unknownReason={appInfoError.message} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (appParamsError) {
|
||||
return <div className='flex h-full items-center justify-center'>
|
||||
<AppUnavailable unknownReason={appParamsError.message} />
|
||||
</div>
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<AppUnavailable unknownReason={appParamsError.message} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (appMetaError) {
|
||||
return <div className='flex h-full items-center justify-center'>
|
||||
<AppUnavailable unknownReason={appMetaError.message} />
|
||||
</div>
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<AppUnavailable unknownReason={appMetaError.message} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (useCanAccessAppError) {
|
||||
return <div className='flex h-full items-center justify-center'>
|
||||
<AppUnavailable unknownReason={useCanAccessAppError.message} />
|
||||
</div>
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<AppUnavailable unknownReason={useCanAccessAppError.message} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (userCanAccessApp && !userCanAccessApp.result) {
|
||||
return <div className='flex h-full flex-col items-center justify-center gap-y-2'>
|
||||
<AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' />
|
||||
<span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>
|
||||
</div>
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-y-2">
|
||||
<AppUnavailable className="h-auto w-auto" code={403} unknownReason="no permission." />
|
||||
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('userProfile.logout', { ns: 'common' })}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta) {
|
||||
return <div className='flex h-full items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <>{children}</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
'use client'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { webAppLoginStatus, webAppLogout } from '@/service/webapp-auth'
|
||||
import { fetchAccessToken } from '@/service/share'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { fetchAccessToken } from '@/service/share'
|
||||
import { setWebAppAccessToken, setWebAppPassport, webAppLoginStatus, webAppLogout } from '@/service/webapp-auth'
|
||||
|
||||
const Splash: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -42,7 +40,7 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
|
|||
return
|
||||
}
|
||||
|
||||
if(tokenFromUrl)
|
||||
if (tokenFromUrl)
|
||||
setWebAppAccessToken(tokenFromUrl)
|
||||
|
||||
const redirectOrFinish = () => {
|
||||
|
|
@ -90,19 +88,24 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
|
|||
message,
|
||||
webAppAccessMode,
|
||||
tokenFromUrl,
|
||||
embeddedUserId])
|
||||
embeddedUserId,
|
||||
])
|
||||
|
||||
if (message) {
|
||||
return <div className='flex h-full flex-col items-center justify-center gap-y-4'>
|
||||
<AppUnavailable className='h-auto w-auto' code={code || t('share.common.appUnavailable')} unknownReason={message} />
|
||||
<span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span>
|
||||
</div>
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-y-4">
|
||||
<AppUnavailable className="h-auto w-auto" code={code || t('common.appUnavailable', { ns: 'share' })} unknownReason={message} />
|
||||
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{code === '403' ? t('userProfile.logout', { ns: 'common' }) : t('login.backToHome', { ns: 'share' })}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <div className='flex h-full items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <>{children}</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
'use client'
|
||||
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useState } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { sendWebAppResetPasswordCode, verifyWebAppResetPasswordCode } from '@/service/common'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { sendWebAppResetPasswordCode, verifyWebAppResetPasswordCode } from '@/service/common'
|
||||
|
||||
export default function CheckCode() {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -26,14 +26,14 @@ export default function CheckCode() {
|
|||
if (!code.trim()) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.checkCode.emptyCode'),
|
||||
message: t('checkCode.emptyCode', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!/\d{6}/.test(code)) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.checkCode.invalidCode'),
|
||||
message: t('checkCode.invalidCode', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -63,37 +63,39 @@ export default function CheckCode() {
|
|||
catch (error) { console.error(error) }
|
||||
}
|
||||
|
||||
return <div className='flex flex-col gap-3'>
|
||||
<div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge text-text-accent-light-mode-only shadow-lg'>
|
||||
<RiMailSendFill className='h-6 w-6 text-2xl' />
|
||||
</div>
|
||||
<div className='pb-4 pt-2'>
|
||||
<h2 className='title-4xl-semi-bold text-text-primary'>{t('login.checkCode.checkYourEmail')}</h2>
|
||||
<p className='body-md-regular mt-2 text-text-secondary'>
|
||||
<span>
|
||||
{t('login.checkCode.tipsPrefix')}
|
||||
<strong>{email}</strong>
|
||||
</span>
|
||||
<br />
|
||||
{t('login.checkCode.validTime')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form action="">
|
||||
<input type='text' className='hidden' />
|
||||
<label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label>
|
||||
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') || ''} />
|
||||
<Button loading={loading} disabled={loading} className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button>
|
||||
<Countdown onResend={resendCode} />
|
||||
</form>
|
||||
<div className='py-2'>
|
||||
<div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
|
||||
</div>
|
||||
<div onClick={() => router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'>
|
||||
<div className='bg-background-default-dimm inline-block rounded-full p-1'>
|
||||
<RiArrowLeftLine size={12} />
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge text-text-accent-light-mode-only shadow-lg">
|
||||
<RiMailSendFill className="h-6 w-6 text-2xl" />
|
||||
</div>
|
||||
<div className="pb-4 pt-2">
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{t('checkCode.checkYourEmail', { ns: 'login' })}</h2>
|
||||
<p className="body-md-regular mt-2 text-text-secondary">
|
||||
<span>
|
||||
{t('checkCode.tipsPrefix', { ns: 'login' })}
|
||||
<strong>{email}</strong>
|
||||
</span>
|
||||
<br />
|
||||
{t('checkCode.validTime', { ns: 'login' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form action="">
|
||||
<input type="text" className="hidden" />
|
||||
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('checkCode.verificationCode', { ns: 'login' })}</label>
|
||||
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className="mt-1" placeholder={t('checkCode.verificationCodePlaceholder', { ns: 'login' }) || ''} />
|
||||
<Button loading={loading} disabled={loading} className="my-3 w-full" variant="primary" onClick={verify}>{t('checkCode.verify', { ns: 'login' })}</Button>
|
||||
<Countdown onResend={resendCode} />
|
||||
</form>
|
||||
<div className="py-2">
|
||||
<div className="h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent"></div>
|
||||
</div>
|
||||
<div onClick={() => router.back()} className="flex h-9 cursor-pointer items-center justify-center text-text-tertiary">
|
||||
<div className="bg-background-default-dimm inline-block rounded-full p-1">
|
||||
<RiArrowLeftLine size={12} />
|
||||
</div>
|
||||
<span className="system-xs-regular ml-2">{t('back', { ns: 'login' })}</span>
|
||||
</div>
|
||||
<span className='system-xs-regular ml-2'>{t('login.back')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,39 @@
|
|||
'use client'
|
||||
import Header from '@/app/signin/_header'
|
||||
|
||||
import cn from '@/utils/classnames'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export default function SignInLayout({ children }: any) {
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
return <>
|
||||
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
||||
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
|
||||
<Header />
|
||||
<div className={
|
||||
cn(
|
||||
'flex w-full grow flex-col items-center justify-center',
|
||||
'px-6',
|
||||
'md:px-[108px]',
|
||||
)
|
||||
}>
|
||||
<div className='flex w-[400px] flex-col'>
|
||||
{children}
|
||||
return (
|
||||
<>
|
||||
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
||||
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
|
||||
<Header />
|
||||
<div className={
|
||||
cn(
|
||||
'flex w-full grow flex-col items-center justify-center',
|
||||
'px-6',
|
||||
'md:px-[108px]',
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex w-[400px] flex-col">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{!systemFeatures.branding.enabled && (
|
||||
<div className="system-xs-regular px-8 py-6 text-text-tertiary">
|
||||
©
|
||||
{' '}
|
||||
{new Date().getFullYear()}
|
||||
{' '}
|
||||
LangGenius, Inc. All rights reserved.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!systemFeatures.branding.enabled && <div className='system-xs-regular px-8 py-6 text-text-tertiary'>
|
||||
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useState } from 'react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||
import { emailRegex } from '@/config'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { sendResetPasswordCode } from '@/service/common'
|
||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||
import { emailRegex } from '@/config'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { noop } from 'lodash-es'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { sendResetPasswordCode } from '@/service/common'
|
||||
|
||||
export default function CheckCode() {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -27,14 +27,14 @@ export default function CheckCode() {
|
|||
const handleGetEMailVerificationCode = async () => {
|
||||
try {
|
||||
if (!email) {
|
||||
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
|
||||
Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) })
|
||||
return
|
||||
}
|
||||
|
||||
if (!emailRegex.test(email)) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.emailInValid'),
|
||||
message: t('error.emailInValid', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ export default function CheckCode() {
|
|||
else if (res.code === 'account_not_found') {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.registrationNotAllowed'),
|
||||
message: t('error.registrationNotAllowed', { ns: 'login' }),
|
||||
})
|
||||
}
|
||||
else {
|
||||
|
|
@ -68,37 +68,39 @@ export default function CheckCode() {
|
|||
}
|
||||
}
|
||||
|
||||
return <div className='flex flex-col gap-3'>
|
||||
<div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg'>
|
||||
<RiLockPasswordLine className='h-6 w-6 text-2xl text-text-accent-light-mode-only' />
|
||||
</div>
|
||||
<div className='pb-4 pt-2'>
|
||||
<h2 className='title-4xl-semi-bold text-text-primary'>{t('login.resetPassword')}</h2>
|
||||
<p className='body-md-regular mt-2 text-text-secondary'>
|
||||
{t('login.resetPasswordDesc')}
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg">
|
||||
<RiLockPasswordLine className="h-6 w-6 text-2xl text-text-accent-light-mode-only" />
|
||||
</div>
|
||||
<div className="pb-4 pt-2">
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{t('resetPassword', { ns: 'login' })}</h2>
|
||||
<p className="body-md-regular mt-2 text-text-secondary">
|
||||
{t('resetPasswordDesc', { ns: 'login' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={noop}>
|
||||
<input type='text' className='hidden' />
|
||||
<div className='mb-2'>
|
||||
<label htmlFor="email" className='system-md-semibold my-2 text-text-secondary'>{t('login.email')}</label>
|
||||
<div className='mt-1'>
|
||||
<Input id='email' type="email" disabled={loading} value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
|
||||
</div>
|
||||
<div className='mt-3'>
|
||||
<Button loading={loading} disabled={loading} variant='primary' className='w-full' onClick={handleGetEMailVerificationCode}>{t('login.sendVerificationCode')}</Button>
|
||||
<form onSubmit={noop}>
|
||||
<input type="text" className="hidden" />
|
||||
<div className="mb-2">
|
||||
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('email', { ns: 'login' })}</label>
|
||||
<div className="mt-1">
|
||||
<Input id="email" type="email" disabled={loading} value={email} placeholder={t('emailPlaceholder', { ns: 'login' }) as string} onChange={e => setEmail(e.target.value)} />
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Button loading={loading} disabled={loading} variant="primary" className="w-full" onClick={handleGetEMailVerificationCode}>{t('sendVerificationCode', { ns: 'login' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="py-2">
|
||||
<div className="h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent"></div>
|
||||
</div>
|
||||
</form>
|
||||
<div className='py-2'>
|
||||
<div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
|
||||
<Link href={`/webapp-signin?${searchParams.toString()}`} className="flex h-9 items-center justify-center text-text-tertiary hover:text-text-primary">
|
||||
<div className="inline-block rounded-full bg-background-default-dimmed p-1">
|
||||
<RiArrowLeftLine size={12} />
|
||||
</div>
|
||||
<span className="system-xs-regular ml-2">{t('backToLogin', { ns: 'login' })}</span>
|
||||
</Link>
|
||||
</div>
|
||||
<Link href={`/webapp-signin?${searchParams.toString()}`} className='flex h-9 items-center justify-center text-text-tertiary hover:text-text-primary'>
|
||||
<div className='inline-block rounded-full bg-background-default-dimmed p-1'>
|
||||
<RiArrowLeftLine size={12} />
|
||||
</div>
|
||||
<span className='system-xs-regular ml-2'>{t('login.backToLogin')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
'use client'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import cn from 'classnames'
|
||||
import { RiCheckboxCircleFill } from '@remixicon/react'
|
||||
import { useCountDown } from 'ahooks'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { changeWebAppPasswordWithToken } from '@/service/common'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { validPassword } from '@/config'
|
||||
import { changeWebAppPasswordWithToken } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const ChangePasswordForm = () => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -45,15 +45,15 @@ const ChangePasswordForm = () => {
|
|||
|
||||
const valid = useCallback(() => {
|
||||
if (!password.trim()) {
|
||||
showErrorMessage(t('login.error.passwordEmpty'))
|
||||
showErrorMessage(t('error.passwordEmpty', { ns: 'login' }))
|
||||
return false
|
||||
}
|
||||
if (!validPassword.test(password)) {
|
||||
showErrorMessage(t('login.error.passwordInvalid'))
|
||||
showErrorMessage(t('error.passwordInvalid', { ns: 'login' }))
|
||||
return false
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
showErrorMessage(t('common.account.notEqual'))
|
||||
showErrorMessage(t('account.notEqual', { ns: 'common' }))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
|
@ -86,62 +86,64 @@ const ChangePasswordForm = () => {
|
|||
'px-6',
|
||||
'md:px-[108px]',
|
||||
)
|
||||
}>
|
||||
}
|
||||
>
|
||||
{!showSuccess && (
|
||||
<div className='flex flex-col md:w-[400px]'>
|
||||
<div className="flex flex-col md:w-[400px]">
|
||||
<div className="mx-auto w-full">
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">
|
||||
{t('login.changePassword')}
|
||||
{t('changePassword', { ns: 'login' })}
|
||||
</h2>
|
||||
<p className='body-md-regular mt-2 text-text-secondary'>
|
||||
{t('login.changePasswordTip')}
|
||||
<p className="body-md-regular mt-2 text-text-secondary">
|
||||
{t('changePasswordTip', { ns: 'login' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-6 w-full">
|
||||
<div className="bg-white">
|
||||
{/* Password */}
|
||||
<div className='mb-5'>
|
||||
<div className="mb-5">
|
||||
<label htmlFor="password" className="system-md-semibold my-2 text-text-secondary">
|
||||
{t('common.account.newPassword')}
|
||||
{t('account.newPassword', { ns: 'common' })}
|
||||
</label>
|
||||
<div className='relative mt-1'>
|
||||
<div className="relative mt-1">
|
||||
<Input
|
||||
id="password" type={showPassword ? 'text' : 'password'}
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
placeholder={t('login.passwordPlaceholder') || ''}
|
||||
placeholder={t('passwordPlaceholder', { ns: 'login' }) || ''}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant='ghost'
|
||||
variant="ghost"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='body-xs-regular mt-1 text-text-secondary'>{t('login.error.passwordInvalid')}</div>
|
||||
<div className="body-xs-regular mt-1 text-text-secondary">{t('error.passwordInvalid', { ns: 'login' })}</div>
|
||||
</div>
|
||||
{/* Confirm Password */}
|
||||
<div className='mb-5'>
|
||||
<div className="mb-5">
|
||||
<label htmlFor="confirmPassword" className="system-md-semibold my-2 text-text-secondary">
|
||||
{t('common.account.confirmPassword')}
|
||||
{t('account.confirmPassword', { ns: 'common' })}
|
||||
</label>
|
||||
<div className='relative mt-1'>
|
||||
<div className="relative mt-1">
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
placeholder={t('login.confirmPasswordPlaceholder') || ''}
|
||||
placeholder={t('confirmPasswordPlaceholder', { ns: 'login' }) || ''}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant='ghost'
|
||||
variant="ghost"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showConfirmPassword ? '👀' : '😝'}
|
||||
|
|
@ -151,11 +153,11 @@ const ChangePasswordForm = () => {
|
|||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full'
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={handleChangePassword}
|
||||
>
|
||||
{t('login.changePasswordBtn')}
|
||||
{t('changePasswordBtn', { ns: 'login' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -166,17 +168,28 @@ const ChangePasswordForm = () => {
|
|||
<div className="flex flex-col md:w-[400px]">
|
||||
<div className="mx-auto w-full">
|
||||
<div className="mb-3 flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle font-bold shadow-lg">
|
||||
<RiCheckboxCircleFill className='h-6 w-6 text-text-success' />
|
||||
<RiCheckboxCircleFill className="h-6 w-6 text-text-success" />
|
||||
</div>
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">
|
||||
{t('login.passwordChangedTip')}
|
||||
{t('passwordChangedTip', { ns: 'login' })}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mx-auto mt-6 w-full">
|
||||
<Button variant='primary' className='w-full' onClick={() => {
|
||||
setLeftTime(undefined)
|
||||
router.replace(getSignInUrl())
|
||||
}}>{t('login.passwordChanged')} ({Math.round(countdown / 1000)}) </Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setLeftTime(undefined)
|
||||
router.replace(getSignInUrl())
|
||||
}}
|
||||
>
|
||||
{t('passwordChanged', { ns: 'login' })}
|
||||
{' '}
|
||||
(
|
||||
{Math.round(countdown / 1000)}
|
||||
)
|
||||
{' '}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
'use client'
|
||||
import type { FormEvent } from 'react'
|
||||
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { type FormEvent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
|
||||
import { fetchAccessToken } from '@/service/share'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common'
|
||||
import { fetchAccessToken } from '@/service/share'
|
||||
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
|
||||
|
||||
export default function CheckCode() {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -44,21 +45,21 @@ export default function CheckCode() {
|
|||
if (!code.trim()) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.checkCode.emptyCode'),
|
||||
message: t('checkCode.emptyCode', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!/\d{6}/.test(code)) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.checkCode.invalidCode'),
|
||||
message: t('checkCode.invalidCode', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!redirectUrl || !appCode) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.redirectUrlMissing'),
|
||||
message: t('error.redirectUrlMissing', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -101,44 +102,46 @@ export default function CheckCode() {
|
|||
catch (error) { console.error(error) }
|
||||
}
|
||||
|
||||
return <div className='flex w-[400px] flex-col gap-3'>
|
||||
<div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg'>
|
||||
<RiMailSendFill className='h-6 w-6 text-2xl text-text-accent-light-mode-only' />
|
||||
</div>
|
||||
<div className='pb-4 pt-2'>
|
||||
<h2 className='title-4xl-semi-bold text-text-primary'>{t('login.checkCode.checkYourEmail')}</h2>
|
||||
<p className='body-md-regular mt-2 text-text-secondary'>
|
||||
<span>
|
||||
{t('login.checkCode.tipsPrefix')}
|
||||
<strong>{email}</strong>
|
||||
</span>
|
||||
<br />
|
||||
{t('login.checkCode.validTime')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label>
|
||||
<Input
|
||||
ref={codeInputRef}
|
||||
id='code'
|
||||
value={code}
|
||||
onChange={e => setVerifyCode(e.target.value)}
|
||||
maxLength={6}
|
||||
className='mt-1'
|
||||
placeholder={t('login.checkCode.verificationCodePlaceholder') || ''}
|
||||
/>
|
||||
<Button type='submit' loading={loading} disabled={loading} className='my-3 w-full' variant='primary'>{t('login.checkCode.verify')}</Button>
|
||||
<Countdown onResend={resendCode} />
|
||||
</form>
|
||||
<div className='py-2'>
|
||||
<div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
|
||||
</div>
|
||||
<div onClick={() => router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'>
|
||||
<div className='bg-background-default-dimm inline-block rounded-full p-1'>
|
||||
<RiArrowLeftLine size={12} />
|
||||
return (
|
||||
<div className="flex w-[400px] flex-col gap-3">
|
||||
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg">
|
||||
<RiMailSendFill className="h-6 w-6 text-2xl text-text-accent-light-mode-only" />
|
||||
</div>
|
||||
<div className="pb-4 pt-2">
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{t('checkCode.checkYourEmail', { ns: 'login' })}</h2>
|
||||
<p className="body-md-regular mt-2 text-text-secondary">
|
||||
<span>
|
||||
{t('checkCode.tipsPrefix', { ns: 'login' })}
|
||||
<strong>{email}</strong>
|
||||
</span>
|
||||
<br />
|
||||
{t('checkCode.validTime', { ns: 'login' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('checkCode.verificationCode', { ns: 'login' })}</label>
|
||||
<Input
|
||||
ref={codeInputRef}
|
||||
id="code"
|
||||
value={code}
|
||||
onChange={e => setVerifyCode(e.target.value)}
|
||||
maxLength={6}
|
||||
className="mt-1"
|
||||
placeholder={t('checkCode.verificationCodePlaceholder', { ns: 'login' }) || ''}
|
||||
/>
|
||||
<Button type="submit" loading={loading} disabled={loading} className="my-3 w-full" variant="primary">{t('checkCode.verify', { ns: 'login' })}</Button>
|
||||
<Countdown onResend={resendCode} />
|
||||
</form>
|
||||
<div className="py-2">
|
||||
<div className="h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent"></div>
|
||||
</div>
|
||||
<div onClick={() => router.back()} className="flex h-9 cursor-pointer items-center justify-center text-text-tertiary">
|
||||
<div className="bg-background-default-dimm inline-block rounded-full p-1">
|
||||
<RiArrowLeftLine size={12} />
|
||||
</div>
|
||||
<span className="system-xs-regular ml-2">{t('back', { ns: 'login' })}</span>
|
||||
</div>
|
||||
<span className='system-xs-regular ml-2'>{t('login.back')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
'use client'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { SSOProtocol } from '@/types/feature'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
|
||||
import { SSOProtocol } from '@/types/feature'
|
||||
|
||||
const ExternalMemberSSOAuth = () => {
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
|
@ -68,9 +69,11 @@ const ExternalMemberSSOAuth = () => {
|
|||
}, [handleSSOLogin])
|
||||
|
||||
if (!systemFeatures.webapp_auth.sso_config.protocol) {
|
||||
return <div className="flex h-full items-center justify-center">
|
||||
<AppUnavailable code={403} unknownReason='sso protocol is invalid.' />
|
||||
</div>
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<AppUnavailable code={403} unknownReason="sso protocol is invalid." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { noop } from 'es-toolkit/compat'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { emailRegex } from '@/config'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { sendWebAppEMailLoginCode } from '@/service/common'
|
||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||
import { emailRegex } from '@/config'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { noop } from 'lodash-es'
|
||||
import { sendWebAppEMailLoginCode } from '@/service/common'
|
||||
|
||||
export default function MailAndCodeAuth() {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -23,14 +23,14 @@ export default function MailAndCodeAuth() {
|
|||
const handleGetEMailVerificationCode = async () => {
|
||||
try {
|
||||
if (!email) {
|
||||
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
|
||||
Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) })
|
||||
return
|
||||
}
|
||||
|
||||
if (!emailRegex.test(email)) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.emailInValid'),
|
||||
message: t('error.emailInValid', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -52,17 +52,18 @@ export default function MailAndCodeAuth() {
|
|||
}
|
||||
}
|
||||
|
||||
return (<form onSubmit={noop}>
|
||||
<input type='text' className='hidden' />
|
||||
<div className='mb-2'>
|
||||
<label htmlFor="email" className='system-md-semibold my-2 text-text-secondary'>{t('login.email')}</label>
|
||||
<div className='mt-1'>
|
||||
<Input id='email' type="email" value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
|
||||
return (
|
||||
<form onSubmit={noop}>
|
||||
<input type="text" className="hidden" />
|
||||
<div className="mb-2">
|
||||
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('email', { ns: 'login' })}</label>
|
||||
<div className="mt-1">
|
||||
<Input id="email" type="email" value={email} placeholder={t('emailPlaceholder', { ns: 'login' }) as string} onChange={e => setEmail(e.target.value)} />
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Button loading={loading} disabled={loading || !email} variant="primary" className="w-full" onClick={handleGetEMailVerificationCode}>{t('signup.verifyMail', { ns: 'login' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-3'>
|
||||
<Button loading={loading} disabled={loading || !email} variant='primary' className='w-full' onClick={handleGetEMailVerificationCode}>{t('login.signup.verifyMail')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
'use client'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { emailRegex } from '@/config'
|
||||
import { webAppLogin } from '@/service/common'
|
||||
import Input from '@/app/components/base/input'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { noop } from 'lodash-es'
|
||||
import { webAppLogin } from '@/service/common'
|
||||
import { fetchAccessToken } from '@/service/share'
|
||||
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
|
||||
|
||||
|
|
@ -46,25 +46,25 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
|
|||
const appCode = getAppCodeFromRedirectUrl()
|
||||
const handleEmailPasswordLogin = async () => {
|
||||
if (!email) {
|
||||
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
|
||||
Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) })
|
||||
return
|
||||
}
|
||||
if (!emailRegex.test(email)) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.emailInValid'),
|
||||
message: t('error.emailInValid', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!password?.trim()) {
|
||||
Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') })
|
||||
Toast.notify({ type: 'error', message: t('error.passwordEmpty', { ns: 'login' }) })
|
||||
return
|
||||
}
|
||||
|
||||
if (!redirectUrl || !appCode) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.redirectUrlMissing'),
|
||||
message: t('error.redirectUrlMissing', { ns: 'login' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -107,70 +107,74 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
|
|||
}
|
||||
}
|
||||
|
||||
return <form onSubmit={noop}>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
|
||||
{t('login.email')}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<Input
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder={t('login.emailPlaceholder') || ''}
|
||||
tabIndex={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label htmlFor="password" className="my-2 flex items-center justify-between">
|
||||
<span className='system-md-semibold text-text-secondary'>{t('login.password')}</span>
|
||||
<Link
|
||||
href={`/webapp-reset-password?${searchParams.toString()}`}
|
||||
className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`}
|
||||
tabIndex={isEmailSetup ? 0 : -1}
|
||||
aria-disabled={!isEmailSetup}
|
||||
>
|
||||
{t('login.forget')}
|
||||
</Link>
|
||||
</label>
|
||||
<div className="relative mt-1">
|
||||
<Input
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
id="password"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter')
|
||||
handleEmailPasswordLogin()
|
||||
}}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="current-password"
|
||||
placeholder={t('login.passwordPlaceholder') || ''}
|
||||
tabIndex={2}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant='ghost'
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
return (
|
||||
<form onSubmit={noop}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
|
||||
{t('email', { ns: 'login' })}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<Input
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder={t('emailPlaceholder', { ns: 'login' }) || ''}
|
||||
tabIndex={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-2'>
|
||||
<Button
|
||||
tabIndex={2}
|
||||
variant='primary'
|
||||
onClick={handleEmailPasswordLogin}
|
||||
disabled={isLoading || !email || !password}
|
||||
className="w-full"
|
||||
>{t('login.signBtn')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="password" className="my-2 flex items-center justify-between">
|
||||
<span className="system-md-semibold text-text-secondary">{t('password', { ns: 'login' })}</span>
|
||||
<Link
|
||||
href={`/webapp-reset-password?${searchParams.toString()}`}
|
||||
className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`}
|
||||
tabIndex={isEmailSetup ? 0 : -1}
|
||||
aria-disabled={!isEmailSetup}
|
||||
>
|
||||
{t('forget', { ns: 'login' })}
|
||||
</Link>
|
||||
</label>
|
||||
<div className="relative mt-1">
|
||||
<Input
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
id="password"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter')
|
||||
handleEmailPasswordLogin()
|
||||
}}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="current-password"
|
||||
placeholder={t('passwordPlaceholder', { ns: 'login' }) || ''}
|
||||
tabIndex={2}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-2">
|
||||
<Button
|
||||
tabIndex={2}
|
||||
variant="primary"
|
||||
onClick={handleEmailPasswordLogin}
|
||||
disabled={isLoading || !email || !password}
|
||||
className="w-full"
|
||||
>
|
||||
{t('signBtn', { ns: 'login' })}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
'use client'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { SSOProtocol } from '@/types/feature'
|
||||
import { fetchMembersOAuth2SSOUrl, fetchMembersOIDCSSOUrl, fetchMembersSAMLSSOUrl } from '@/service/share'
|
||||
import { SSOProtocol } from '@/types/feature'
|
||||
|
||||
type SSOAuthProps = {
|
||||
protocol: SSOProtocol | ''
|
||||
|
|
@ -82,8 +81,8 @@ const SSOAuth: FC<SSOAuthProps> = ({
|
|||
disabled={isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
<Lock01 className='mr-2 h-5 w-5 text-text-accent-light-mode-only' />
|
||||
<span className="truncate">{t('login.withSSO')}</span>
|
||||
<Lock01 className="mr-2 h-5 w-5 text-text-accent-light-mode-only" />
|
||||
<span className="truncate">{t('withSSO', { ns: 'login' })}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,36 @@
|
|||
'use client'
|
||||
|
||||
import cn from '@/utils/classnames'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export default function SignInLayout({ children }: PropsWithChildren) {
|
||||
const { t } = useTranslation()
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
useDocumentTitle(t('login.webapp.login'))
|
||||
return <>
|
||||
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
||||
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
|
||||
{/* <Header /> */}
|
||||
<div className={cn('flex w-full grow flex-col items-center justify-center px-6 md:px-[108px]')}>
|
||||
<div className='flex justify-center md:w-[440px] lg:w-[600px]'>
|
||||
{children}
|
||||
useDocumentTitle(t('webapp.login', { ns: 'login' }))
|
||||
return (
|
||||
<>
|
||||
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
||||
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
|
||||
{/* <Header /> */}
|
||||
<div className={cn('flex w-full grow flex-col items-center justify-center px-6 md:px-[108px]')}>
|
||||
<div className="flex justify-center md:w-[440px] lg:w-[600px]">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{systemFeatures.branding.enabled === false && (
|
||||
<div className="system-xs-regular px-8 py-6 text-text-tertiary">
|
||||
©
|
||||
{' '}
|
||||
{new Date().getFullYear()}
|
||||
{' '}
|
||||
LangGenius, Inc. All rights reserved.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{systemFeatures.branding.enabled === false && <div className='system-xs-regular px-8 py-6 text-text-tertiary'>
|
||||
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
'use client'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react'
|
||||
import Link from 'next/link'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { LicenseStatus } from '@/types/feature'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import MailAndCodeAuth from './components/mail-and-code-auth'
|
||||
import MailAndPasswordAuth from './components/mail-and-password-auth'
|
||||
import SSOAuth from './components/sso-auth'
|
||||
import cn from '@/utils/classnames'
|
||||
import { LicenseStatus } from '@/types/feature'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
const NormalForm = () => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -37,136 +38,173 @@ const NormalForm = () => {
|
|||
init()
|
||||
}, [init])
|
||||
if (isLoading) {
|
||||
return <div className={
|
||||
cn(
|
||||
'flex w-full grow flex-col items-center justify-center',
|
||||
'px-6',
|
||||
'md:px-[108px]',
|
||||
)
|
||||
}>
|
||||
<Loading type='area' />
|
||||
</div>
|
||||
return (
|
||||
<div className={
|
||||
cn(
|
||||
'flex w-full grow flex-col items-center justify-center',
|
||||
'px-6',
|
||||
'md:px-[108px]',
|
||||
)
|
||||
}
|
||||
>
|
||||
<Loading type="area" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (systemFeatures.license?.status === LicenseStatus.LOST) {
|
||||
return <div className='mx-auto mt-8 w-full'>
|
||||
<div className='relative'>
|
||||
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
|
||||
<div className='shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
|
||||
<RiContractLine className='h-5 w-5' />
|
||||
<RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' />
|
||||
return (
|
||||
<div className="mx-auto mt-8 w-full">
|
||||
<div className="relative">
|
||||
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
|
||||
<div className="shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow">
|
||||
<RiContractLine className="h-5 w-5" />
|
||||
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
|
||||
</div>
|
||||
<p className="system-sm-medium text-text-primary">{t('licenseLost', { ns: 'login' })}</p>
|
||||
<p className="system-xs-regular mt-1 text-text-tertiary">{t('licenseLostTip', { ns: 'login' })}</p>
|
||||
</div>
|
||||
<p className='system-sm-medium text-text-primary'>{t('login.licenseLost')}</p>
|
||||
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.licenseLostTip')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (systemFeatures.license?.status === LicenseStatus.EXPIRED) {
|
||||
return <div className='mx-auto mt-8 w-full'>
|
||||
<div className='relative'>
|
||||
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
|
||||
<div className='shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
|
||||
<RiContractLine className='h-5 w-5' />
|
||||
<RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' />
|
||||
return (
|
||||
<div className="mx-auto mt-8 w-full">
|
||||
<div className="relative">
|
||||
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
|
||||
<div className="shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow">
|
||||
<RiContractLine className="h-5 w-5" />
|
||||
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
|
||||
</div>
|
||||
<p className="system-sm-medium text-text-primary">{t('licenseExpired', { ns: 'login' })}</p>
|
||||
<p className="system-xs-regular mt-1 text-text-tertiary">{t('licenseExpiredTip', { ns: 'login' })}</p>
|
||||
</div>
|
||||
<p className='system-sm-medium text-text-primary'>{t('login.licenseExpired')}</p>
|
||||
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.licenseExpiredTip')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (systemFeatures.license?.status === LicenseStatus.INACTIVE) {
|
||||
return <div className='mx-auto mt-8 w-full'>
|
||||
<div className='relative'>
|
||||
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
|
||||
<div className='shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
|
||||
<RiContractLine className='h-5 w-5' />
|
||||
<RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' />
|
||||
return (
|
||||
<div className="mx-auto mt-8 w-full">
|
||||
<div className="relative">
|
||||
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
|
||||
<div className="shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow">
|
||||
<RiContractLine className="h-5 w-5" />
|
||||
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
|
||||
</div>
|
||||
<p className="system-sm-medium text-text-primary">{t('licenseInactive', { ns: 'login' })}</p>
|
||||
<p className="system-xs-regular mt-1 text-text-tertiary">{t('licenseInactiveTip', { ns: 'login' })}</p>
|
||||
</div>
|
||||
<p className='system-sm-medium text-text-primary'>{t('login.licenseInactive')}</p>
|
||||
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.licenseInactiveTip')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto mt-8 w-full">
|
||||
<div className="mx-auto w-full">
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{systemFeatures.branding.enabled ? t('login.pageTitleForE') : t('login.pageTitle')}</h2>
|
||||
<p className='body-md-regular mt-2 text-text-tertiary'>{t('login.welcome')}</p>
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{systemFeatures.branding.enabled ? t('pageTitleForE', { ns: 'login' }) : t('pageTitle', { ns: 'login' })}</h2>
|
||||
<p className="body-md-regular mt-2 text-text-tertiary">{t('welcome', { ns: 'login' })}</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="mt-6 flex flex-col gap-3">
|
||||
{systemFeatures.sso_enforced_for_signin && <div className='w-full'>
|
||||
<SSOAuth protocol={systemFeatures.sso_enforced_for_signin_protocol} />
|
||||
</div>}
|
||||
{systemFeatures.sso_enforced_for_signin && (
|
||||
<div className="w-full">
|
||||
<SSOAuth protocol={systemFeatures.sso_enforced_for_signin_protocol} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showORLine && <div className="relative mt-6">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className='h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('login.or')}</span>
|
||||
</div>
|
||||
</div>}
|
||||
{
|
||||
(systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login) && <>
|
||||
{systemFeatures.enable_email_code_login && authType === 'code' && <>
|
||||
<MailAndCodeAuth />
|
||||
{systemFeatures.enable_email_password_login && <div className='cursor-pointer py-1 text-center' onClick={() => { updateAuthType('password') }}>
|
||||
<span className='system-xs-medium text-components-button-secondary-accent-text'>{t('login.usePassword')}</span>
|
||||
</div>}
|
||||
</>}
|
||||
{systemFeatures.enable_email_password_login && authType === 'password' && <>
|
||||
<MailAndPasswordAuth isEmailSetup={systemFeatures.is_email_setup} />
|
||||
{systemFeatures.enable_email_code_login && <div className='cursor-pointer py-1 text-center' onClick={() => { updateAuthType('code') }}>
|
||||
<span className='system-xs-medium text-components-button-secondary-accent-text'>{t('login.useVerificationCode')}</span>
|
||||
</div>}
|
||||
</>}
|
||||
</>
|
||||
}
|
||||
{allMethodsAreDisabled && <>
|
||||
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
|
||||
<div className='shadows-shadow-lg mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
|
||||
<RiDoorLockLine className='h-5 w-5' />
|
||||
</div>
|
||||
<p className='system-sm-medium text-text-primary'>{t('login.noLoginMethod')}</p>
|
||||
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.noLoginMethodTip')}</p>
|
||||
</div>
|
||||
<div className="relative my-2 py-2">
|
||||
{showORLine && (
|
||||
<div className="relative mt-6">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className='h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
|
||||
<div className="h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('or', { ns: 'login' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>}
|
||||
{!systemFeatures.branding.enabled && <>
|
||||
<div className="system-xs-regular mt-2 block w-full text-text-tertiary">
|
||||
{t('login.tosDesc')}
|
||||
)}
|
||||
{
|
||||
(systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login) && (
|
||||
<>
|
||||
{systemFeatures.enable_email_code_login && authType === 'code' && (
|
||||
<>
|
||||
<MailAndCodeAuth />
|
||||
{systemFeatures.enable_email_password_login && (
|
||||
<div className="cursor-pointer py-1 text-center" onClick={() => { updateAuthType('password') }}>
|
||||
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('usePassword', { ns: 'login' })}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{systemFeatures.enable_email_password_login && authType === 'password' && (
|
||||
<>
|
||||
<MailAndPasswordAuth isEmailSetup={systemFeatures.is_email_setup} />
|
||||
{systemFeatures.enable_email_code_login && (
|
||||
<div className="cursor-pointer py-1 text-center" onClick={() => { updateAuthType('code') }}>
|
||||
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('useVerificationCode', { ns: 'login' })}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{allMethodsAreDisabled && (
|
||||
<>
|
||||
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
|
||||
<div className="shadows-shadow-lg mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow">
|
||||
<RiDoorLockLine className="h-5 w-5" />
|
||||
</div>
|
||||
<p className="system-sm-medium text-text-primary">{t('noLoginMethod', { ns: 'login' })}</p>
|
||||
<p className="system-xs-regular mt-1 text-text-tertiary">{t('noLoginMethodTip', { ns: 'login' })}</p>
|
||||
</div>
|
||||
<div className="relative my-2 py-2">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!systemFeatures.branding.enabled && (
|
||||
<>
|
||||
<div className="system-xs-regular mt-2 block w-full text-text-tertiary">
|
||||
{t('tosDesc', { ns: 'login' })}
|
||||
|
||||
<Link
|
||||
className='system-xs-medium text-text-secondary hover:underline'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://dify.ai/terms'
|
||||
>{t('login.tos')}</Link>
|
||||
<Link
|
||||
className="system-xs-medium text-text-secondary hover:underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://dify.ai/terms"
|
||||
>
|
||||
{t('tos', { ns: 'login' })}
|
||||
</Link>
|
||||
&
|
||||
<Link
|
||||
className='system-xs-medium text-text-secondary hover:underline'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://dify.ai/privacy'
|
||||
>{t('login.pp')}</Link>
|
||||
</div>
|
||||
{IS_CE_EDITION && <div className="w-hull system-xs-regular mt-2 block text-text-tertiary">
|
||||
{t('login.goToInit')}
|
||||
<Link
|
||||
className="system-xs-medium text-text-secondary hover:underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://dify.ai/privacy"
|
||||
>
|
||||
{t('pp', { ns: 'login' })}
|
||||
</Link>
|
||||
</div>
|
||||
{IS_CE_EDITION && (
|
||||
<div className="w-hull system-xs-regular mt-2 block text-text-tertiary">
|
||||
{t('goToInit', { ns: 'login' })}
|
||||
|
||||
<Link
|
||||
className='system-xs-medium text-text-secondary hover:underline'
|
||||
href='/install'
|
||||
>{t('login.setAdminAccount')}</Link>
|
||||
</div>}
|
||||
</>}
|
||||
<Link
|
||||
className="system-xs-medium text-text-secondary hover:underline"
|
||||
href="/install"
|
||||
>
|
||||
{t('setAdminAccount', { ns: 'login' })}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
'use client'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
import NormalForm from './normalForm'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import ExternalMemberSsoAuth from './components/external-member-sso-auth'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { webAppLogout } from '@/service/webapp-auth'
|
||||
import ExternalMemberSsoAuth from './components/external-member-sso-auth'
|
||||
import NormalForm from './normalForm'
|
||||
|
||||
const WebSSOForm: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -34,29 +35,37 @@ const WebSSOForm: FC = () => {
|
|||
}, [getSigninUrl, router, webAppLogout, shareCode])
|
||||
|
||||
if (!redirectUrl) {
|
||||
return <div className='flex h-full items-center justify-center'>
|
||||
<AppUnavailable code={t('share.common.appUnavailable')} unknownReason='redirect url is invalid.' />
|
||||
</div>
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<AppUnavailable code={t('common.appUnavailable', { ns: 'share' })} unknownReason="redirect url is invalid." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!systemFeatures.webapp_auth.enabled) {
|
||||
return <div className="flex h-full items-center justify-center">
|
||||
<p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p>
|
||||
</div>
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="system-xs-regular text-text-tertiary">{t('webapp.disabled', { ns: 'login' })}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (webAppAccessMode && (webAppAccessMode === AccessMode.ORGANIZATION || webAppAccessMode === AccessMode.SPECIFIC_GROUPS_MEMBERS)) {
|
||||
return <div className='w-full max-w-[400px]'>
|
||||
<NormalForm />
|
||||
</div>
|
||||
return (
|
||||
<div className="w-full max-w-[400px]">
|
||||
<NormalForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (webAppAccessMode && webAppAccessMode === AccessMode.EXTERNAL_MEMBERS)
|
||||
return <ExternalMemberSsoAuth />
|
||||
|
||||
return <div className='flex h-full flex-col items-center justify-center gap-y-4'>
|
||||
<AppUnavailable className='h-auto w-auto' isUnknownReason={true} />
|
||||
<span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('share.login.backToHome')}</span>
|
||||
</div>
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-y-4">
|
||||
<AppUnavailable className="h-auto w-auto" isUnknownReason={true} />
|
||||
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('login.backToHome', { ns: 'share' })}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(WebSSOForm)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
|
||||
import Main from '@/app/components/share/text-generation'
|
||||
import AuthenticatedLayout from '../../components/authenticated-layout'
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
'use client'
|
||||
|
||||
import type { Area } from 'react-easy-crop'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import type { OnImageInput } from '@/app/components/base/app-icon-picker/ImageInput'
|
||||
import type { AvatarProps } from '@/app/components/base/avatar'
|
||||
import type { ImageFile } from '@/types/app'
|
||||
import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import ImageInput, { type OnImageInput } from '@/app/components/base/app-icon-picker/ImageInput'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Avatar, { type AvatarProps } from '@/app/components/base/avatar'
|
||||
import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks'
|
||||
import type { ImageFile } from '@/types/app'
|
||||
import ImageInput from '@/app/components/base/app-icon-picker/ImageInput'
|
||||
import getCroppedImg from '@/app/components/base/app-icon-picker/utils'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
|
||||
type InputImageInfo = { file: File } | { tempUrl: string; croppedAreaPixels: Area; fileName: string }
|
||||
type InputImageInfo = { file: File } | { tempUrl: string, croppedAreaPixels: Area, fileName: string }
|
||||
type AvatarWithEditProps = AvatarProps & { onSave?: () => void }
|
||||
|
||||
const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
||||
|
|
@ -45,7 +48,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
|||
await updateUserProfile({ url: 'account/avatar', body: { avatar: uploadedFileId } })
|
||||
setIsShowAvatarPicker(false)
|
||||
onSave?.()
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: (e as Error).message })
|
||||
|
|
@ -55,7 +58,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
|||
const handleDeleteAvatar = useCallback(async () => {
|
||||
try {
|
||||
await updateUserProfile({ url: 'account/avatar', body: { avatar: '' } })
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
setIsShowDeleteConfirm(false)
|
||||
onSave?.()
|
||||
}
|
||||
|
|
@ -116,15 +119,17 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
|||
setHoverArea(isRight ? 'right' : 'left')
|
||||
}}
|
||||
>
|
||||
{hoverArea === 'right' && !onAvatarError ? (
|
||||
<span className="text-xs text-white">
|
||||
<RiDeleteBin5Line />
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-white">
|
||||
<RiPencilLine />
|
||||
</span>
|
||||
)}
|
||||
{hoverArea === 'right' && !onAvatarError
|
||||
? (
|
||||
<span className="text-xs text-white">
|
||||
<RiDeleteBin5Line />
|
||||
</span>
|
||||
)
|
||||
: (
|
||||
<span className="text-xs text-white">
|
||||
<RiPencilLine />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -135,16 +140,16 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
|||
isShow={isShowAvatarPicker}
|
||||
onClose={() => setIsShowAvatarPicker(false)}
|
||||
>
|
||||
<ImageInput onImageInput={handleImageInput} cropShape='round' />
|
||||
<Divider className='m-0' />
|
||||
<ImageInput onImageInput={handleImageInput} cropShape="round" />
|
||||
<Divider className="m-0" />
|
||||
|
||||
<div className='flex w-full items-center justify-center gap-2 p-3'>
|
||||
<Button className='w-full' onClick={() => setIsShowAvatarPicker(false)}>
|
||||
{t('app.iconPicker.cancel')}
|
||||
<div className="flex w-full items-center justify-center gap-2 p-3">
|
||||
<Button className="w-full" onClick={() => setIsShowAvatarPicker(false)}>
|
||||
{t('iconPicker.cancel', { ns: 'app' })}
|
||||
</Button>
|
||||
|
||||
<Button variant="primary" className='w-full' disabled={uploading || !inputImageInfo} loading={uploading} onClick={handleSelect}>
|
||||
{t('app.iconPicker.ok')}
|
||||
<Button variant="primary" className="w-full" disabled={uploading || !inputImageInfo} loading={uploading} onClick={handleSelect}>
|
||||
{t('iconPicker.ok', { ns: 'app' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
@ -155,16 +160,16 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
|||
isShow={isShowDeleteConfirm}
|
||||
onClose={() => setIsShowDeleteConfirm(false)}
|
||||
>
|
||||
<div className="title-2xl-semi-bold mb-3 text-text-primary">{t('common.avatar.deleteTitle')}</div>
|
||||
<p className="mb-8 text-text-secondary">{t('common.avatar.deleteDescription')}</p>
|
||||
<div className="title-2xl-semi-bold mb-3 text-text-primary">{t('avatar.deleteTitle', { ns: 'common' })}</div>
|
||||
<p className="mb-8 text-text-secondary">{t('avatar.deleteDescription', { ns: 'common' })}</p>
|
||||
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<Button className="w-full" onClick={() => setIsShowDeleteConfirm(false)}>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
|
||||
<Button variant="warning" className="w-full" onClick={handleDeleteAvatar}>
|
||||
{t('common.operation.delete')}
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
import React, { useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import type { ResponseError } from '@/service/fetch'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import {
|
||||
checkEmailExisted,
|
||||
resetEmail,
|
||||
sendVerifyCode,
|
||||
verifyEmail,
|
||||
} from '@/service/common'
|
||||
import { noop } from 'lodash-es'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import type { ResponseError } from '@/service/fetch'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
|
||||
type Props = {
|
||||
show: boolean
|
||||
|
|
@ -116,7 +117,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||
}
|
||||
|
||||
const isValidEmail = (email: string): boolean => {
|
||||
const rfc5322emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
|
||||
const rfc5322emailRegex = /^[\w.!#$%&'*+/=?^`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i
|
||||
return rfc5322emailRegex.test(email) && email.length <= 254
|
||||
}
|
||||
|
||||
|
|
@ -201,176 +202,176 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||
<Modal
|
||||
isShow={show}
|
||||
onClose={noop}
|
||||
className='!w-[420px] !p-6'
|
||||
className="!w-[420px] !p-6"
|
||||
>
|
||||
<div className='absolute right-5 top-5 cursor-pointer p-1.5' onClick={onClose}>
|
||||
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
|
||||
<div className="absolute right-5 top-5 cursor-pointer p-1.5" onClick={onClose}>
|
||||
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
|
||||
</div>
|
||||
{step === STEP.start && (
|
||||
<>
|
||||
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.title')}</div>
|
||||
<div className='space-y-0.5 pb-2 pt-1'>
|
||||
<div className='body-md-medium text-text-warning'>{t('common.account.changeEmail.authTip')}</div>
|
||||
<div className='body-md-regular text-text-secondary'>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.title', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-medium text-text-warning">{t('account.changeEmail.authTip', { ns: 'common' })}</div>
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey="common.account.changeEmail.content1"
|
||||
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
|
||||
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
|
||||
values={{ email }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='pt-3'></div>
|
||||
<div className='space-y-2'>
|
||||
<div className="pt-3"></div>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
className='!w-full'
|
||||
variant='primary'
|
||||
className="!w-full"
|
||||
variant="primary"
|
||||
onClick={sendCodeToOriginEmail}
|
||||
>
|
||||
{t('common.account.changeEmail.sendVerifyCode')}
|
||||
{t('account.changeEmail.sendVerifyCode', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className='!w-full'
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.verifyOrigin && (
|
||||
<>
|
||||
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.verifyEmail')}</div>
|
||||
<div className='space-y-0.5 pb-2 pt-1'>
|
||||
<div className='body-md-regular text-text-secondary'>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.verifyEmail', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey="common.account.changeEmail.content2"
|
||||
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
|
||||
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
|
||||
values={{ email }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='pt-3'>
|
||||
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.account.changeEmail.codeLabel')}</div>
|
||||
<div className="pt-3">
|
||||
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className='!w-full'
|
||||
placeholder={t('common.account.changeEmail.codePlaceholder')}
|
||||
className="!w-full"
|
||||
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
|
||||
value={code}
|
||||
onChange={e => setCode(e.target.value)}
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-3 space-y-2'>
|
||||
<div className="mt-3 space-y-2">
|
||||
<Button
|
||||
disabled={code.length !== 6}
|
||||
className='!w-full'
|
||||
variant='primary'
|
||||
className="!w-full"
|
||||
variant="primary"
|
||||
onClick={handleVerifyOriginEmail}
|
||||
>
|
||||
{t('common.account.changeEmail.continue')}
|
||||
{t('account.changeEmail.continue', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className='!w-full'
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className='system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary'>
|
||||
<span>{t('common.account.changeEmail.resendTip')}</span>
|
||||
<div className="system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary">
|
||||
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
|
||||
{time > 0 && (
|
||||
<span>{t('common.account.changeEmail.resendCount', { count: time })}</span>
|
||||
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
|
||||
)}
|
||||
{!time && (
|
||||
<span onClick={sendCodeToOriginEmail} className='system-xs-medium cursor-pointer text-text-accent-secondary'>{t('common.account.changeEmail.resend')}</span>
|
||||
<span onClick={sendCodeToOriginEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('account.changeEmail.resend', { ns: 'common' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.newEmail && (
|
||||
<>
|
||||
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.newEmail')}</div>
|
||||
<div className='space-y-0.5 pb-2 pt-1'>
|
||||
<div className='body-md-regular text-text-secondary'>{t('common.account.changeEmail.content3')}</div>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.newEmail', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-regular text-text-secondary">{t('account.changeEmail.content3', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<div className='pt-3'>
|
||||
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.account.changeEmail.emailLabel')}</div>
|
||||
<div className="pt-3">
|
||||
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('account.changeEmail.emailLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className='!w-full'
|
||||
placeholder={t('common.account.changeEmail.emailPlaceholder')}
|
||||
className="!w-full"
|
||||
placeholder={t('account.changeEmail.emailPlaceholder', { ns: 'common' })}
|
||||
value={mail}
|
||||
onChange={e => handleNewEmailValueChange(e.target.value)}
|
||||
destructive={newEmailExited || unAvailableEmail}
|
||||
/>
|
||||
{newEmailExited && (
|
||||
<div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.existingEmail')}</div>
|
||||
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('account.changeEmail.existingEmail', { ns: 'common' })}</div>
|
||||
)}
|
||||
{unAvailableEmail && (
|
||||
<div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.unAvailableEmail')}</div>
|
||||
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-3 space-y-2'>
|
||||
<div className="mt-3 space-y-2">
|
||||
<Button
|
||||
disabled={!mail || newEmailExited || unAvailableEmail || isCheckingEmail || !isValidEmail(mail)}
|
||||
className='!w-full'
|
||||
variant='primary'
|
||||
className="!w-full"
|
||||
variant="primary"
|
||||
onClick={sendCodeToNewEmail}
|
||||
>
|
||||
{t('common.account.changeEmail.sendVerifyCode')}
|
||||
{t('account.changeEmail.sendVerifyCode', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className='!w-full'
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.verifyNew && (
|
||||
<>
|
||||
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.verifyNew')}</div>
|
||||
<div className='space-y-0.5 pb-2 pt-1'>
|
||||
<div className='body-md-regular text-text-secondary'>
|
||||
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('account.changeEmail.verifyNew', { ns: 'common' })}</div>
|
||||
<div className="space-y-0.5 pb-2 pt-1">
|
||||
<div className="body-md-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey="common.account.changeEmail.content4"
|
||||
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
|
||||
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
|
||||
values={{ email: mail }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='pt-3'>
|
||||
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.account.changeEmail.codeLabel')}</div>
|
||||
<div className="pt-3">
|
||||
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className='!w-full'
|
||||
placeholder={t('common.account.changeEmail.codePlaceholder')}
|
||||
className="!w-full"
|
||||
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
|
||||
value={code}
|
||||
onChange={e => setCode(e.target.value)}
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-3 space-y-2'>
|
||||
<div className="mt-3 space-y-2">
|
||||
<Button
|
||||
disabled={code.length !== 6}
|
||||
className='!w-full'
|
||||
variant='primary'
|
||||
className="!w-full"
|
||||
variant="primary"
|
||||
onClick={submitNewEmail}
|
||||
>
|
||||
{t('common.account.changeEmail.changeTo', { email: mail })}
|
||||
{t('account.changeEmail.changeTo', { ns: 'common', email: mail })}
|
||||
</Button>
|
||||
<Button
|
||||
className='!w-full'
|
||||
className="!w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className='system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary'>
|
||||
<span>{t('common.account.changeEmail.resendTip')}</span>
|
||||
<div className="system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary">
|
||||
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
|
||||
{time > 0 && (
|
||||
<span>{t('common.account.changeEmail.resendCount', { count: time })}</span>
|
||||
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
|
||||
)}
|
||||
{!time && (
|
||||
<span onClick={sendCodeToNewEmail} className='system-xs-medium cursor-pointer text-text-accent-secondary'>{t('common.account.changeEmail.resend')}</span>
|
||||
<span onClick={sendCodeToNewEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('account.changeEmail.resend', { ns: 'common' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,30 +1,29 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { IItem } from '@/app/components/header/account-setting/collapse'
|
||||
import type { App } from '@/types/app'
|
||||
import {
|
||||
RiGraduationCapFill,
|
||||
} from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import DeleteAccount from '../delete-account'
|
||||
import AvatarWithEdit from './AvatarWithEdit'
|
||||
import Collapse from '@/app/components/header/account-setting/collapse'
|
||||
import type { IItem } from '@/app/components/header/account-setting/collapse'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import Collapse from '@/app/components/header/account-setting/collapse'
|
||||
import { IS_CE_EDITION, validPassword } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import EmailChangeModal from './email-change-modal'
|
||||
import { validPassword } from '@/config'
|
||||
|
||||
import type { App } from '@/types/app'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
import { useAppList } from '@/service/use-apps'
|
||||
import DeleteAccount from '../delete-account'
|
||||
|
||||
import AvatarWithEdit from './AvatarWithEdit'
|
||||
import EmailChangeModal from './email-change-modal'
|
||||
|
||||
const titleClassName = `
|
||||
system-sm-semibold text-text-secondary
|
||||
|
|
@ -62,7 +61,7 @@ export default function AccountPage() {
|
|||
try {
|
||||
setEditing(true)
|
||||
await updateUserProfile({ url: 'account/name', body: { name: editName } })
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
mutateUserProfile()
|
||||
setEditNameModalVisible(false)
|
||||
setEditing(false)
|
||||
|
|
@ -81,15 +80,15 @@ export default function AccountPage() {
|
|||
}
|
||||
const valid = () => {
|
||||
if (!password.trim()) {
|
||||
showErrorMessage(t('login.error.passwordEmpty'))
|
||||
showErrorMessage(t('error.passwordEmpty', { ns: 'login' }))
|
||||
return false
|
||||
}
|
||||
if (!validPassword.test(password)) {
|
||||
showErrorMessage(t('login.error.passwordInvalid'))
|
||||
showErrorMessage(t('error.passwordInvalid', { ns: 'login' }))
|
||||
return false
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
showErrorMessage(t('common.account.notEqual'))
|
||||
showErrorMessage(t('account.notEqual', { ns: 'common' }))
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +112,7 @@ export default function AccountPage() {
|
|||
repeat_new_password: confirmPassword,
|
||||
},
|
||||
})
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
mutateUserProfile()
|
||||
setEditPasswordModalVisible(false)
|
||||
resetPasswordForm()
|
||||
|
|
@ -129,111 +128,112 @@ export default function AccountPage() {
|
|||
const renderAppItem = (item: IItem) => {
|
||||
const { icon, icon_background, icon_type, icon_url } = item as any
|
||||
return (
|
||||
<div className='flex px-3 py-1'>
|
||||
<div className='mr-3'>
|
||||
<div className="flex px-3 py-1">
|
||||
<div className="mr-3">
|
||||
<AppIcon
|
||||
size='tiny'
|
||||
size="tiny"
|
||||
iconType={icon_type}
|
||||
icon={icon}
|
||||
background={icon_background}
|
||||
imageUrl={icon_url}
|
||||
/>
|
||||
</div>
|
||||
<div className='system-sm-medium mt-[3px] text-text-secondary'>{item.name}</div>
|
||||
<div className="system-sm-medium mt-[3px] text-text-secondary">{item.name}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='pb-3 pt-2'>
|
||||
<h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
|
||||
<div className="pb-3 pt-2">
|
||||
<h4 className="title-2xl-semi-bold text-text-primary">{t('account.myAccount', { ns: 'common' })}</h4>
|
||||
</div>
|
||||
<div className='mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6'>
|
||||
<div className="mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6">
|
||||
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={mutateUserProfile} size={64} />
|
||||
<div className='ml-4'>
|
||||
<p className='system-xl-semibold text-text-primary'>
|
||||
<div className="ml-4">
|
||||
<p className="system-xl-semibold text-text-primary">
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
|
||||
<RiGraduationCapFill className='mr-1 h-3 w-3' />
|
||||
<span className='system-2xs-medium'>EDU</span>
|
||||
<PremiumBadge size="s" color="blue" className="ml-1 !px-2">
|
||||
<RiGraduationCapFill className="mr-1 h-3 w-3" />
|
||||
<span className="system-2xs-medium">EDU</span>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</p>
|
||||
<p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>
|
||||
<p className="system-xs-regular text-text-tertiary">{userProfile.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-8'>
|
||||
<div className={titleClassName}>{t('common.account.name')}</div>
|
||||
<div className='mt-2 flex w-full items-center justify-between gap-2'>
|
||||
<div className='system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled '>
|
||||
<span className='pl-1'>{userProfile.name}</span>
|
||||
<div className="mb-8">
|
||||
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
|
||||
<div className="mt-2 flex w-full items-center justify-between gap-2">
|
||||
<div className="system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled ">
|
||||
<span className="pl-1">{userProfile.name}</span>
|
||||
</div>
|
||||
<div className='system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text' onClick={handleEditName}>
|
||||
{t('common.operation.edit')}
|
||||
<div className="system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text" onClick={handleEditName}>
|
||||
{t('operation.edit', { ns: 'common' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-8'>
|
||||
<div className={titleClassName}>{t('common.account.email')}</div>
|
||||
<div className='mt-2 flex w-full items-center justify-between gap-2'>
|
||||
<div className='system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled '>
|
||||
<span className='pl-1'>{userProfile.email}</span>
|
||||
<div className="mb-8">
|
||||
<div className={titleClassName}>{t('account.email', { ns: 'common' })}</div>
|
||||
<div className="mt-2 flex w-full items-center justify-between gap-2">
|
||||
<div className="system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled ">
|
||||
<span className="pl-1">{userProfile.email}</span>
|
||||
</div>
|
||||
{systemFeatures.enable_change_email && (
|
||||
<div className='system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text' onClick={() => setShowUpdateEmail(true)}>
|
||||
{t('common.operation.change')}
|
||||
<div className="system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text" onClick={() => setShowUpdateEmail(true)}>
|
||||
{t('operation.change', { ns: 'common' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
systemFeatures.enable_email_password_login && (
|
||||
<div className='mb-8 flex justify-between gap-2'>
|
||||
<div className="mb-8 flex justify-between gap-2">
|
||||
<div>
|
||||
<div className='system-sm-semibold mb-1 text-text-secondary'>{t('common.account.password')}</div>
|
||||
<div className='body-xs-regular mb-2 text-text-tertiary'>{t('common.account.passwordTip')}</div>
|
||||
<div className="system-sm-semibold mb-1 text-text-secondary">{t('account.password', { ns: 'common' })}</div>
|
||||
<div className="body-xs-regular mb-2 text-text-tertiary">{t('account.passwordTip', { ns: 'common' })}</div>
|
||||
</div>
|
||||
<Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</Button>
|
||||
<Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='mb-6 border-[1px] border-divider-subtle' />
|
||||
<div className='mb-8'>
|
||||
<div className={titleClassName}>{t('common.account.langGeniusAccount')}</div>
|
||||
<div className={descriptionClassName}>{t('common.account.langGeniusAccountTip')}</div>
|
||||
<div className="mb-6 border-[1px] border-divider-subtle" />
|
||||
<div className="mb-8">
|
||||
<div className={titleClassName}>{t('account.langGeniusAccount', { ns: 'common' })}</div>
|
||||
<div className={descriptionClassName}>{t('account.langGeniusAccountTip', { ns: 'common' })}</div>
|
||||
{!!apps.length && (
|
||||
<Collapse
|
||||
title={`${t('common.account.showAppLength', { length: apps.length })}`}
|
||||
title={`${t('account.showAppLength', { ns: 'common', length: apps.length })}`}
|
||||
items={apps.map((app: App) => ({ ...app, key: app.id, name: app.name }))}
|
||||
renderItem={renderAppItem}
|
||||
wrapperClassName='mt-2'
|
||||
wrapperClassName="mt-2"
|
||||
/>
|
||||
)}
|
||||
{!IS_CE_EDITION && <Button className='mt-2 text-components-button-destructive-secondary-text' onClick={() => setShowDeleteAccountModal(true)}>{t('common.account.delete')}</Button>}
|
||||
{!IS_CE_EDITION && <Button className="mt-2 text-components-button-destructive-secondary-text" onClick={() => setShowDeleteAccountModal(true)}>{t('account.delete', { ns: 'common' })}</Button>}
|
||||
</div>
|
||||
{
|
||||
editNameModalVisible && (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={() => setEditNameModalVisible(false)}
|
||||
className='!w-[420px] !p-6'
|
||||
className="!w-[420px] !p-6"
|
||||
>
|
||||
<div className='title-2xl-semi-bold mb-6 text-text-primary'>{t('common.account.editName')}</div>
|
||||
<div className={titleClassName}>{t('common.account.name')}</div>
|
||||
<Input className='mt-2'
|
||||
<div className="title-2xl-semi-bold mb-6 text-text-primary">{t('account.editName', { ns: 'common' })}</div>
|
||||
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
|
||||
<Input
|
||||
className="mt-2"
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
/>
|
||||
<div className='mt-10 flex justify-end'>
|
||||
<Button className='mr-2' onClick={() => setEditNameModalVisible(false)}>{t('common.operation.cancel')}</Button>
|
||||
<div className="mt-10 flex justify-end">
|
||||
<Button className="mr-2" onClick={() => setEditNameModalVisible(false)}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button
|
||||
disabled={editing || !editName}
|
||||
variant='primary'
|
||||
variant="primary"
|
||||
onClick={handleSaveName}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
@ -247,13 +247,13 @@ export default function AccountPage() {
|
|||
setEditPasswordModalVisible(false)
|
||||
resetPasswordForm()
|
||||
}}
|
||||
className='!w-[420px] !p-6'
|
||||
className="!w-[420px] !p-6"
|
||||
>
|
||||
<div className='title-2xl-semi-bold mb-6 text-text-primary'>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div>
|
||||
<div className="title-2xl-semi-bold mb-6 text-text-primary">{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</div>
|
||||
{userProfile.is_password_set && (
|
||||
<>
|
||||
<div className={titleClassName}>{t('common.account.currentPassword')}</div>
|
||||
<div className='relative mt-2'>
|
||||
<div className={titleClassName}>{t('account.currentPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showCurrentPassword ? 'text' : 'password'}
|
||||
value={currentPassword}
|
||||
|
|
@ -263,7 +263,7 @@ export default function AccountPage() {
|
|||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant='ghost'
|
||||
variant="ghost"
|
||||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
||||
>
|
||||
{showCurrentPassword ? '👀' : '😝'}
|
||||
|
|
@ -272,10 +272,10 @@ export default function AccountPage() {
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className='system-sm-semibold mt-8 text-text-secondary'>
|
||||
{userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')}
|
||||
<div className="system-sm-semibold mt-8 text-text-secondary">
|
||||
{userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })}
|
||||
</div>
|
||||
<div className='relative mt-2'>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
|
|
@ -284,15 +284,15 @@ export default function AccountPage() {
|
|||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant='ghost'
|
||||
variant="ghost"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='system-sm-semibold mt-8 text-text-secondary'>{t('common.account.confirmPassword')}</div>
|
||||
<div className='relative mt-2'>
|
||||
<div className="system-sm-semibold mt-8 text-text-secondary">{t('account.confirmPassword', { ns: 'common' })}</div>
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassword}
|
||||
|
|
@ -301,24 +301,29 @@ export default function AccountPage() {
|
|||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant='ghost'
|
||||
variant="ghost"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showConfirmPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-10 flex justify-end'>
|
||||
<Button className='mr-2' onClick={() => {
|
||||
setEditPasswordModalVisible(false)
|
||||
resetPasswordForm()
|
||||
}}>{t('common.operation.cancel')}</Button>
|
||||
<div className="mt-10 flex justify-end">
|
||||
<Button
|
||||
className="mr-2"
|
||||
onClick={() => {
|
||||
setEditPasswordModalVisible(false)
|
||||
resetPasswordForm()
|
||||
}}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={editing}
|
||||
variant='primary'
|
||||
variant="primary"
|
||||
onClick={handleSavePassword}
|
||||
>
|
||||
{userProfile.is_password_set ? t('common.operation.reset') : t('common.operation.save')}
|
||||
{userProfile.is_password_set ? t('operation.reset', { ns: 'common' }) : t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Fragment } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
|
||||
import {
|
||||
RiGraduationCapFill,
|
||||
} from '@remixicon/react'
|
||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Fragment } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||
|
||||
export type IAppSelector = {
|
||||
isMobile: boolean
|
||||
|
|
@ -70,31 +70,31 @@ export default function AppSelector() {
|
|||
"
|
||||
>
|
||||
<MenuItem>
|
||||
<div className='p-1'>
|
||||
<div className='flex flex-nowrap items-center px-3 py-2'>
|
||||
<div className='grow'>
|
||||
<div className='system-md-medium break-all text-text-primary'>
|
||||
<div className="p-1">
|
||||
<div className="flex flex-nowrap items-center px-3 py-2">
|
||||
<div className="grow">
|
||||
<div className="system-md-medium break-all text-text-primary">
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
|
||||
<RiGraduationCapFill className='mr-1 h-3 w-3' />
|
||||
<span className='system-2xs-medium'>EDU</span>
|
||||
<PremiumBadge size="s" color="blue" className="ml-1 !px-2">
|
||||
<RiGraduationCapFill className="mr-1 h-3 w-3" />
|
||||
<span className="system-2xs-medium">EDU</span>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</div>
|
||||
<div className='system-xs-regular break-all text-text-tertiary'>{userProfile.email}</div>
|
||||
<div className="system-xs-regular break-all text-text-tertiary">{userProfile.email}</div>
|
||||
</div>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={32} />
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<div className='p-1' onClick={() => handleLogout()}>
|
||||
<div className="p-1" onClick={() => handleLogout()}>
|
||||
<div
|
||||
className='group flex h-9 cursor-pointer items-center justify-start rounded-lg px-3 hover:bg-state-base-hover'
|
||||
className="group flex h-9 cursor-pointer items-center justify-start rounded-lg px-3 hover:bg-state-base-hover"
|
||||
>
|
||||
<LogOut01 className='mr-1 flex h-4 w-4 text-text-tertiary' />
|
||||
<div className='text-[14px] font-normal text-text-secondary'>{t('common.userProfile.logout')}</div>
|
||||
<LogOut01 className="mr-1 flex h-4 w-4 text-text-tertiary" />
|
||||
<div className="text-[14px] font-normal text-text-secondary">{t('userProfile.logout', { ns: 'common' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useSendDeleteAccountEmail } from '../state'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useSendDeleteAccountEmail } from '../state'
|
||||
|
||||
type DeleteAccountProps = {
|
||||
onCancel: () => void
|
||||
|
|
@ -28,21 +28,26 @@ export default function CheckEmail(props: DeleteAccountProps) {
|
|||
catch (error) { console.error(error) }
|
||||
}, [getDeleteEmailVerifyCode, props])
|
||||
|
||||
return <>
|
||||
<div className='body-md-medium py-1 text-text-destructive'>
|
||||
{t('common.account.deleteTip')}
|
||||
</div>
|
||||
<div className='body-md-regular pb-2 pt-1 text-text-secondary'>
|
||||
{t('common.account.deletePrivacyLinkTip')}
|
||||
<Link href='https://dify.ai/privacy' className='text-text-accent'>{t('common.account.deletePrivacyLink')}</Link>
|
||||
</div>
|
||||
<label className='system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary'>{t('common.account.deleteLabel')}</label>
|
||||
<Input placeholder={t('common.account.deletePlaceholder') as string} onChange={(e) => {
|
||||
setUserInputEmail(e.target.value)
|
||||
}} />
|
||||
<div className='mt-3 flex w-full flex-col gap-2'>
|
||||
<Button className='w-full' disabled={userInputEmail !== userProfile.email || isSendingEmail} loading={isSendingEmail} variant='primary' onClick={handleConfirm}>{t('common.account.sendVerificationButton')}</Button>
|
||||
<Button className='w-full' onClick={props.onCancel}>{t('common.operation.cancel')}</Button>
|
||||
</div>
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
<div className="body-md-medium py-1 text-text-destructive">
|
||||
{t('account.deleteTip', { ns: 'common' })}
|
||||
</div>
|
||||
<div className="body-md-regular pb-2 pt-1 text-text-secondary">
|
||||
{t('account.deletePrivacyLinkTip', { ns: 'common' })}
|
||||
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('account.deletePrivacyLink', { ns: 'common' })}</Link>
|
||||
</div>
|
||||
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('account.deleteLabel', { ns: 'common' })}</label>
|
||||
<Input
|
||||
placeholder={t('account.deletePlaceholder', { ns: 'common' }) as string}
|
||||
onChange={(e) => {
|
||||
setUserInputEmail(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<div className="mt-3 flex w-full flex-col gap-2">
|
||||
<Button className="w-full" disabled={userInputEmail !== userProfile.email || isSendingEmail} loading={isSendingEmail} variant="primary" onClick={handleConfirm}>{t('account.sendVerificationButton', { ns: 'common' })}</Button>
|
||||
<Button className="w-full" onClick={props.onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useDeleteAccountFeedback } from '../state'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import CustomDialog from '@/app/components/base/dialog'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { useDeleteAccountFeedback } from '../state'
|
||||
|
||||
type DeleteAccountProps = {
|
||||
onCancel: () => void
|
||||
|
|
@ -28,7 +28,7 @@ export default function FeedBack(props: DeleteAccountProps) {
|
|||
await logout()
|
||||
// Tokens are now stored in cookies and cleared by backend
|
||||
router.push('/signin')
|
||||
Toast.notify({ type: 'info', message: t('common.account.deleteSuccessTip') })
|
||||
Toast.notify({ type: 'info', message: t('account.deleteSuccessTip', { ns: 'common' }) })
|
||||
}
|
||||
catch (error) { console.error(error) }
|
||||
}, [router, t])
|
||||
|
|
@ -46,20 +46,27 @@ export default function FeedBack(props: DeleteAccountProps) {
|
|||
props.onCancel()
|
||||
handleSuccess()
|
||||
}, [handleSuccess, props])
|
||||
return <CustomDialog
|
||||
show={true}
|
||||
onClose={props.onCancel}
|
||||
title={t('common.account.feedbackTitle')}
|
||||
className="max-w-[480px]"
|
||||
footer={false}
|
||||
>
|
||||
<label className='system-sm-semibold mb-1 mt-3 flex items-center text-text-secondary'>{t('common.account.feedbackLabel')}</label>
|
||||
<Textarea rows={6} value={userFeedback} placeholder={t('common.account.feedbackPlaceholder') as string} onChange={(e) => {
|
||||
setUserFeedback(e.target.value)
|
||||
}} />
|
||||
<div className='mt-3 flex w-full flex-col gap-2'>
|
||||
<Button className='w-full' loading={isPending} variant='primary' onClick={handleSubmit}>{t('common.operation.submit')}</Button>
|
||||
<Button className='w-full' onClick={handleSkip}>{t('common.operation.skip')}</Button>
|
||||
</div>
|
||||
</CustomDialog>
|
||||
return (
|
||||
<CustomDialog
|
||||
show={true}
|
||||
onClose={props.onCancel}
|
||||
title={t('account.feedbackTitle', { ns: 'common' })}
|
||||
className="max-w-[480px]"
|
||||
footer={false}
|
||||
>
|
||||
<label className="system-sm-semibold mb-1 mt-3 flex items-center text-text-secondary">{t('account.feedbackLabel', { ns: 'common' })}</label>
|
||||
<Textarea
|
||||
rows={6}
|
||||
value={userFeedback}
|
||||
placeholder={t('account.feedbackPlaceholder', { ns: 'common' }) as string}
|
||||
onChange={(e) => {
|
||||
setUserFeedback(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<div className="mt-3 flex w-full flex-col gap-2">
|
||||
<Button className="w-full" loading={isPending} variant="primary" onClick={handleSubmit}>{t('operation.submit', { ns: 'common' })}</Button>
|
||||
<Button className="w-full" onClick={handleSkip}>{t('operation.skip', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</CustomDialog>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useAccountDeleteStore, useConfirmDeleteAccount, useSendDeleteAccountEmail } from '../state'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import { useAccountDeleteStore, useConfirmDeleteAccount, useSendDeleteAccountEmail } from '../state'
|
||||
|
||||
const CODE_EXP = /[A-Za-z\d]{6}/gi
|
||||
const CODE_EXP = /[A-Z\d]{6}/gi
|
||||
|
||||
type DeleteAccountProps = {
|
||||
onCancel: () => void
|
||||
|
|
@ -34,22 +34,29 @@ export default function VerifyEmail(props: DeleteAccountProps) {
|
|||
}
|
||||
catch (error) { console.error(error) }
|
||||
}, [emailToken, verificationCode, confirmDeleteAccount, props])
|
||||
return <>
|
||||
<div className='body-md-medium pt-1 text-text-destructive'>
|
||||
{t('common.account.deleteTip')}
|
||||
</div>
|
||||
<div className='body-md-regular pb-2 pt-1 text-text-secondary'>
|
||||
{t('common.account.deletePrivacyLinkTip')}
|
||||
<Link href='https://dify.ai/privacy' className='text-text-accent'>{t('common.account.deletePrivacyLink')}</Link>
|
||||
</div>
|
||||
<label className='system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary'>{t('common.account.verificationLabel')}</label>
|
||||
<Input minLength={6} maxLength={6} placeholder={t('common.account.verificationPlaceholder') as string} onChange={(e) => {
|
||||
setVerificationCode(e.target.value)
|
||||
}} />
|
||||
<div className='mt-3 flex w-full flex-col gap-2'>
|
||||
<Button className='w-full' disabled={shouldButtonDisabled} loading={isDeleting} variant='warning' onClick={handleConfirm}>{t('common.account.permanentlyDeleteButton')}</Button>
|
||||
<Button className='w-full' onClick={props.onCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Countdown onResend={sendEmail} />
|
||||
</div>
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
<div className="body-md-medium pt-1 text-text-destructive">
|
||||
{t('account.deleteTip', { ns: 'common' })}
|
||||
</div>
|
||||
<div className="body-md-regular pb-2 pt-1 text-text-secondary">
|
||||
{t('account.deletePrivacyLinkTip', { ns: 'common' })}
|
||||
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('account.deletePrivacyLink', { ns: 'common' })}</Link>
|
||||
</div>
|
||||
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('account.verificationLabel', { ns: 'common' })}</label>
|
||||
<Input
|
||||
minLength={6}
|
||||
maxLength={6}
|
||||
placeholder={t('account.verificationPlaceholder', { ns: 'common' }) as string}
|
||||
onChange={(e) => {
|
||||
setVerificationCode(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<div className="mt-3 flex w-full flex-col gap-2">
|
||||
<Button className="w-full" disabled={shouldButtonDisabled} loading={isDeleting} variant="warning" onClick={handleConfirm}>{t('account.permanentlyDeleteButton', { ns: 'common' })}</Button>
|
||||
<Button className="w-full" onClick={props.onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Countdown onResend={sendEmail} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useState } from 'react'
|
||||
import CheckEmail from './components/check-email'
|
||||
import VerifyEmail from './components/verify-email'
|
||||
import FeedBack from './components/feed-back'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CustomDialog from '@/app/components/base/dialog'
|
||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||
import CheckEmail from './components/check-email'
|
||||
import FeedBack from './components/feed-back'
|
||||
import VerifyEmail from './components/verify-email'
|
||||
|
||||
type DeleteAccountProps = {
|
||||
onCancel: () => void
|
||||
|
|
@ -29,16 +29,23 @@ export default function DeleteAccount(props: DeleteAccountProps) {
|
|||
if (showFeedbackDialog)
|
||||
return <FeedBack onCancel={props.onCancel} onConfirm={props.onConfirm} />
|
||||
|
||||
return <CustomDialog
|
||||
show={true}
|
||||
onClose={props.onCancel}
|
||||
title={t('common.account.delete')}
|
||||
className="max-w-[480px]"
|
||||
footer={false}
|
||||
>
|
||||
{!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />}
|
||||
{showVerifyEmail && <VerifyEmail onCancel={props.onCancel} onConfirm={() => {
|
||||
setShowFeedbackDialog(true)
|
||||
}} />}
|
||||
</CustomDialog>
|
||||
return (
|
||||
<CustomDialog
|
||||
show={true}
|
||||
onClose={props.onCancel}
|
||||
title={t('account.delete', { ns: 'common' })}
|
||||
className="max-w-[480px]"
|
||||
footer={false}
|
||||
>
|
||||
{!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />}
|
||||
{showVerifyEmail && (
|
||||
<VerifyEmail
|
||||
onCancel={props.onCancel}
|
||||
onConfirm={() => {
|
||||
setShowFeedbackDialog(true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</CustomDialog>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue