mirror of
https://github.com/langgenius/dify.git
synced 2026-07-14 08:57:02 +08:00
Merge branch 'main' of github.com:langgenius/dify into db-session-annotation
This commit is contained in:
@ -1,30 +0,0 @@
|
||||
# HITL timeout semantics implementation report
|
||||
|
||||
## What changed
|
||||
|
||||
- Updated `api/core/workflow/nodes/human_input/callback.py` so `DifyHITLCallback` now preserves Dify's timeout split at the boundary:
|
||||
- `HumanInputFormStatus.TIMEOUT` returns the graphon timeout branch via `Expired(selected_handle="__timeout__", ...)`.
|
||||
- `HumanInputFormStatus.EXPIRED` is treated as an invalid resume state and raises `AssertionError`.
|
||||
- `HumanInputFormStatus.WAITING` with a past global deadline is treated as an invalid resume state and raises `AssertionError`.
|
||||
- `HumanInputFormStatus.WAITING` with only the node-level deadline expired still returns the timeout branch.
|
||||
- Added `created_at` to `HumanInputFormEntity` and `_HumanInputFormEntityImpl` so the callback can compute the global deadline using Dify's shared `HUMAN_INPUT_GLOBAL_TIMEOUT_SECONDS` invariant.
|
||||
- Kept the submitted and pause flows unchanged.
|
||||
- Added focused unit coverage in `api/tests/unit_tests/core/workflow/test_human_input_callback.py` for:
|
||||
- node timeout branch
|
||||
- global expiration rejection
|
||||
- waiting-form past node deadline timeout
|
||||
- waiting-form past global deadline rejection
|
||||
|
||||
## Verification
|
||||
|
||||
- `uv run --project api pytest -o addopts='' api/tests/unit_tests/core/workflow/test_human_input_callback.py api/tests/unit_tests/core/workflow/nodes/human_input/test_human_input_form_filled_event.py -q`
|
||||
- `git diff --check`
|
||||
|
||||
## Result
|
||||
|
||||
- The focused test set is expected to pass with the new `created_at` boundary in place.
|
||||
- No unrelated files were modified.
|
||||
|
||||
## Concerns
|
||||
|
||||
- The callback now fails fast on invalid resume states by design. That is intentional, but any caller that previously relied on `EXPIRED` being mapped to the timeout branch will now see an assertion failure instead.
|
||||
@ -3,8 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@ -475,13 +476,24 @@ class TestWorkflowTrace:
|
||||
assert span_call[1]["start_time"] == _T0
|
||||
assert span_call[1]["end_time"] == _T1
|
||||
|
||||
def test_emits_companion_log_with_event_name(self, trace_handler: EnterpriseOtelTrace, mock_exporter):
|
||||
with patch("enterprise.telemetry.enterprise_trace.emit_telemetry_log") as mock_log:
|
||||
def test_emits_companion_log_with_event_name(
|
||||
self,
|
||||
trace_handler: EnterpriseOtelTrace,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
with caplog.at_level(logging.INFO, logger="dify.telemetry"):
|
||||
trace_handler._workflow_trace(make_workflow_info())
|
||||
|
||||
mock_log.assert_called_once()
|
||||
assert mock_log.call_args[1]["event_name"] == EnterpriseTelemetryEvent.WORKFLOW_RUN
|
||||
assert mock_log.call_args[1]["tenant_id"] == "tenant-abc"
|
||||
records = [record for record in caplog.records if record.name == "dify.telemetry"]
|
||||
|
||||
assert len(records) == 1
|
||||
record = records[0]
|
||||
|
||||
attrs = cast(dict[str, Any], record.__dict__["attributes"])
|
||||
|
||||
assert attrs["dify.event.name"] == EnterpriseTelemetryEvent.WORKFLOW_RUN
|
||||
assert attrs["dify.event.signal"] == "span_detail"
|
||||
assert record.__dict__["tenant_id"] == "tenant-abc"
|
||||
|
||||
def test_companion_log_includes_content_when_enabled(self, trace_handler: EnterpriseOtelTrace, mock_exporter):
|
||||
mock_exporter.include_content = True
|
||||
|
||||
@ -113,6 +113,21 @@ describe('Registry (pure)', () => {
|
||||
expect(active?.ctx.account.email).toBe('a@x')
|
||||
})
|
||||
|
||||
it('resolveActive returns the active context with insecureTls', () => {
|
||||
const reg = baseReg()
|
||||
reg.upsert('h1', 'a@x', ctx('a@x'))
|
||||
reg.setInsecureTls('h1', true)
|
||||
reg.setHost('h1')
|
||||
reg.setAccount('a@x')
|
||||
expect(reg.resolveActive()?.insecureTls).toBe(true)
|
||||
})
|
||||
|
||||
it('setInsecureTls is a no-op for an unknown host', () => {
|
||||
const reg = baseReg()
|
||||
reg.setInsecureTls('missing', true)
|
||||
expect(reg.hosts.missing).toBeUndefined()
|
||||
})
|
||||
|
||||
it('resolveActive returns undefined for each missing pointer', () => {
|
||||
const reg = baseReg()
|
||||
expect(reg.resolveActive()).toBeUndefined()
|
||||
|
||||
@ -41,6 +41,7 @@ export type AccountContext = z.infer<typeof AccountContextSchema>
|
||||
|
||||
export const HostEntrySchema = z.object({
|
||||
scheme: z.string().optional(),
|
||||
insecure_tls: z.boolean().optional(),
|
||||
current_account: z.string().optional(),
|
||||
accounts: z.record(z.string(), AccountContextSchema).default({}),
|
||||
})
|
||||
@ -58,6 +59,7 @@ export type ActiveContext = {
|
||||
readonly email: string
|
||||
readonly ctx: AccountContext
|
||||
readonly scheme?: string
|
||||
readonly insecureTls?: boolean
|
||||
}
|
||||
|
||||
export function notLoggedInError(hint = 'run \'difyctl auth login\''): BaseError {
|
||||
@ -104,7 +106,7 @@ export class Registry {
|
||||
const ctx = entry.accounts[email]
|
||||
if (ctx === undefined)
|
||||
return undefined
|
||||
return { host, email, ctx, scheme: entry.scheme }
|
||||
return { host, email, ctx, scheme: entry.scheme, insecureTls: entry.insecure_tls }
|
||||
}
|
||||
|
||||
requireActive(hint?: string): ActiveContext {
|
||||
@ -157,6 +159,12 @@ export class Registry {
|
||||
entry.scheme = scheme
|
||||
}
|
||||
|
||||
setInsecureTls(host: string, insecure: boolean): void {
|
||||
const entry = this.data.hosts[host]
|
||||
if (entry !== undefined)
|
||||
entry.insecure_tls = insecure
|
||||
}
|
||||
|
||||
activate(host: string, email: string, ctx: AccountContext): void {
|
||||
this.upsert(host, email, ctx)
|
||||
this.setHost(host)
|
||||
|
||||
@ -13,7 +13,7 @@ import { formatErrorForCli } from '@/errors/format'
|
||||
import { createHttpClient } from '@/http/client'
|
||||
import { getTokenStore } from '@/store/manager'
|
||||
import { realStreams } from '@/sys/io/streams'
|
||||
import { hostWithScheme, openAPIBase } from '@/util/host'
|
||||
import { activeHostInfo, openAPIBase } from '@/util/host'
|
||||
import { enforceDifyVersion } from '@/version/enforce'
|
||||
import { versionInfo } from '@/version/info'
|
||||
import { maybeNudgeCompat } from '@/version/nudge'
|
||||
@ -50,17 +50,17 @@ export async function buildAuthedContext(
|
||||
if (bearer === '')
|
||||
fail(cmd, opts, io)
|
||||
|
||||
const host = hostWithScheme(active.host, active.scheme)
|
||||
const { host, insecure } = activeHostInfo(active)
|
||||
const retryAttempts = resolveRetryAttempts({ flag: opts.retryFlag, env: getEnv })
|
||||
const http = createHttpClient({ baseURL: openAPIBase(host), bearer, retryAttempts })
|
||||
const http = createHttpClient({ baseURL: openAPIBase(host), bearer, retryAttempts, insecure })
|
||||
|
||||
const cache = opts.withCache === true ? await loadAppInfoCache() : undefined
|
||||
|
||||
// Hard gate: refuse a server too old for this difyctl (throws → exit 6).
|
||||
// Cached per host (1h) so most commands don't re-probe. Then the soft nudge
|
||||
// handles the "server too new" direction.
|
||||
await enforceDifyVersion(host)
|
||||
await runCompatNudge({ host, io })
|
||||
await enforceDifyVersion(host, { insecure })
|
||||
await runCompatNudge({ host, insecure, io })
|
||||
|
||||
return { reg, active, store, http, host, io, cache }
|
||||
}
|
||||
@ -74,6 +74,7 @@ function fail(cmd: Pick<Command, 'error'>, opts: AuthedContextOptions, io: IOStr
|
||||
// command flows through it without per-command wiring.
|
||||
async function runCompatNudge(opts: {
|
||||
readonly host: string
|
||||
readonly insecure: boolean
|
||||
readonly io: IOStreams
|
||||
}): Promise<void> {
|
||||
try {
|
||||
@ -81,7 +82,7 @@ async function runCompatNudge(opts: {
|
||||
await maybeNudgeCompat(opts.host, {
|
||||
store,
|
||||
probe: async (host) => {
|
||||
const http = createHttpClient({ baseURL: openAPIBase(host), timeoutMs: META_PROBE_TIMEOUT_MS, retryAttempts: 0 })
|
||||
const http = createHttpClient({ baseURL: openAPIBase(host), timeoutMs: META_PROBE_TIMEOUT_MS, retryAttempts: 0, insecure: opts.insecure })
|
||||
return new MetaClient(http).serverVersion()
|
||||
},
|
||||
emit: line => opts.io.err.write(line),
|
||||
|
||||
@ -27,7 +27,7 @@ export default class Login extends DifyCommand {
|
||||
default: false,
|
||||
}),
|
||||
'insecure': Flags.boolean({
|
||||
description: 'allow http:// hosts (local-dev only)',
|
||||
description: 'allow http:// hosts and skip TLS certificate verification (local-dev only)',
|
||||
default: false,
|
||||
}),
|
||||
}
|
||||
@ -40,7 +40,7 @@ export default class Login extends DifyCommand {
|
||||
noBrowser: flags['no-browser'],
|
||||
insecure: flags.insecure,
|
||||
verifyServer: async (host) => {
|
||||
await enforceDifyVersion(host, { forceFresh: true })
|
||||
await enforceDifyVersion(host, { forceFresh: true, insecure: flags.insecure })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ export async function runLogin(opts: LoginOptions): Promise<Registry> {
|
||||
const host = await resolveLoginHost(opts, insecure)
|
||||
const label = opts.deviceLabel ?? defaultDeviceLabel()
|
||||
|
||||
const api = opts.api ?? new DeviceFlowApi(createHttpClient({ baseURL: openAPIBase(host) }))
|
||||
const api = opts.api ?? new DeviceFlowApi(createHttpClient({ baseURL: openAPIBase(host), insecure }))
|
||||
const code = await api.requestCode({ device_label: label })
|
||||
|
||||
renderCodePrompt(opts.io.err, cs, code)
|
||||
@ -88,6 +88,7 @@ export async function runLogin(opts: LoginOptions): Promise<Registry> {
|
||||
reg.token_storage = storeBundle.mode
|
||||
reg.activate(display, email, ctx)
|
||||
applyScheme(reg, display, host)
|
||||
reg.setInsecureTls(display, insecure)
|
||||
await reg.save()
|
||||
|
||||
renderLoggedIn(opts.io.out, cs, host, success)
|
||||
|
||||
@ -6,7 +6,7 @@ import { createHttpClient } from '@/http/client'
|
||||
import { getTokenStore } from '@/store/manager'
|
||||
import { runWithSpinner } from '@/sys/io/spinner'
|
||||
import { realStreams } from '@/sys/io/streams'
|
||||
import { hostWithScheme, openAPIBase } from '@/util/host'
|
||||
import { activeHostInfo, openAPIBase } from '@/util/host'
|
||||
import { runLogout } from './logout.js'
|
||||
|
||||
export default class Logout extends DifyCommand {
|
||||
@ -32,7 +32,8 @@ export default class Logout extends DifyCommand {
|
||||
}
|
||||
catch { /* keyring locked — skip remote revocation, local cleanup still runs */ }
|
||||
if (bearer !== '') {
|
||||
http = createHttpClient({ baseURL: openAPIBase(hostWithScheme(active.host, active.scheme)), bearer, retryAttempts: 0 })
|
||||
const { host, insecure } = activeHostInfo(active)
|
||||
http = createHttpClient({ baseURL: openAPIBase(host), bearer, retryAttempts: 0, insecure })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
66
cli/src/http/client-tls.test.ts
Normal file
66
cli/src/http/client-tls.test.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import type { Buffer } from 'node:buffer'
|
||||
import type { AddressInfo } from 'node:net'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import * as https from 'node:https'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { createHttpClient } from './client.js'
|
||||
|
||||
function generateSelfSignedCert(dir: string): { key: Buffer, cert: Buffer } {
|
||||
const keyPath = join(dir, 'key.pem')
|
||||
const certPath = join(dir, 'cert.pem')
|
||||
execFileSync('openssl', [
|
||||
'req',
|
||||
'-x509',
|
||||
'-newkey',
|
||||
'rsa:2048',
|
||||
'-nodes',
|
||||
'-keyout',
|
||||
keyPath,
|
||||
'-out',
|
||||
certPath,
|
||||
'-days',
|
||||
'1',
|
||||
'-subj',
|
||||
'/CN=localhost',
|
||||
], { stdio: ['ignore', 'ignore', 'pipe'] })
|
||||
return { key: readFileSync(keyPath), cert: readFileSync(certPath) }
|
||||
}
|
||||
|
||||
// A real server, not a fetch mock, so this also covers Bun's native `tls`
|
||||
// fetch option (ignored by Node, which only reads undici's `dispatcher`).
|
||||
describe('createHttpClient against a real self-signed TLS server', () => {
|
||||
let server: https.Server
|
||||
let baseURL: string
|
||||
let certDir: string
|
||||
|
||||
beforeAll(async () => {
|
||||
certDir = mkdtempSync(join(tmpdir(), 'difyctl-tls-test-'))
|
||||
const { key, cert } = generateSelfSignedCert(certDir)
|
||||
server = https.createServer({ key, cert }, (_req, res) => {
|
||||
res.writeHead(200, { 'content-type': 'application/json' })
|
||||
res.end(JSON.stringify({ ok: true }))
|
||||
})
|
||||
await new Promise<void>(resolve => server.listen(0, resolve))
|
||||
const port = (server.address() as AddressInfo).port
|
||||
baseURL = `https://localhost:${port}/`
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await new Promise<void>(resolve => server.close(() => resolve()))
|
||||
rmSync(certDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('rejects the self-signed cert by default', async () => {
|
||||
const http = createHttpClient({ baseURL, retryAttempts: 0 })
|
||||
await expect(http.get('')).rejects.toBeDefined()
|
||||
})
|
||||
|
||||
it('accepts the self-signed cert when insecure: true', async () => {
|
||||
const http = createHttpClient({ baseURL, retryAttempts: 0, insecure: true })
|
||||
const res = await http.get<{ ok: boolean }>('')
|
||||
expect(res.ok).toBe(true)
|
||||
})
|
||||
})
|
||||
@ -38,6 +38,7 @@ type ClientState = {
|
||||
readonly logger: HttpLogger | undefined
|
||||
readonly originalOptions: ClientOptions
|
||||
readonly dispatcher: ReturnType<typeof proxyDispatcher>
|
||||
readonly insecure: boolean
|
||||
}
|
||||
|
||||
function toArray<T>(value: T | T[] | undefined): T[] {
|
||||
@ -74,7 +75,8 @@ function compileState(opts: ClientOptions): ClientState {
|
||||
hooks: { onRequest, onResponse, onRequestError, onResponseError },
|
||||
logger: opts.logger,
|
||||
originalOptions: opts,
|
||||
dispatcher: proxyDispatcher(),
|
||||
dispatcher: proxyDispatcher({ insecure: opts.insecure }),
|
||||
insecure: opts.insecure ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,9 +173,16 @@ async function execute(
|
||||
|
||||
await runHooks(state.hooks.onRequest, ctx)
|
||||
|
||||
const init: RequestInit & { dispatcher?: unknown, verbose?: boolean } = { signal }
|
||||
// Two runtimes, two options: Node's fetch reads undici's `dispatcher` (used
|
||||
// below for TLS-skip + proxy routing); Bun's native fetch — what the compiled
|
||||
// difyctl binary actually runs on — ignores `dispatcher` entirely and instead
|
||||
// needs its own `tls` option. Set both; each runtime ignores the one it
|
||||
// doesn't understand.
|
||||
const init: RequestInit & { dispatcher?: unknown, tls?: { rejectUnauthorized: boolean }, verbose?: boolean } = { signal }
|
||||
if (state.dispatcher !== undefined)
|
||||
init.dispatcher = state.dispatcher
|
||||
if (state.insecure)
|
||||
init.tls = { rejectUnauthorized: false }
|
||||
if (isVerbose())
|
||||
init.verbose = true
|
||||
|
||||
|
||||
@ -52,4 +52,28 @@ describe('proxyDispatcher', () => {
|
||||
expect(proxyDispatcher()).toBe(first)
|
||||
await first?.close()
|
||||
})
|
||||
|
||||
it('builds a plain Agent with TLS verification disabled when insecure is set and no proxy env', async () => {
|
||||
const { proxyDispatcher } = await import('./proxy.js')
|
||||
const d = proxyDispatcher({ insecure: true })
|
||||
expect(d?.constructor.name).toBe('Agent')
|
||||
await d?.close()
|
||||
})
|
||||
|
||||
it('builds an EnvHttpProxyAgent with TLS verification disabled when insecure and a proxy are both set', async () => {
|
||||
process.env.HTTP_PROXY = 'http://127.0.0.1:8888'
|
||||
const { proxyDispatcher } = await import('./proxy.js')
|
||||
const d = proxyDispatcher({ insecure: true })
|
||||
expect(d?.constructor.name).toBe('EnvHttpProxyAgent')
|
||||
await d?.close()
|
||||
})
|
||||
|
||||
it('re-resolves when the insecure flag changes for the same call site', async () => {
|
||||
const { proxyDispatcher } = await import('./proxy.js')
|
||||
const secure = proxyDispatcher({ insecure: false })
|
||||
expect(secure).toBeUndefined()
|
||||
const insecure = proxyDispatcher({ insecure: true })
|
||||
expect(insecure?.constructor.name).toBe('Agent')
|
||||
await insecure?.close()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { EnvHttpProxyAgent } from 'undici'
|
||||
import type { Dispatcher } from 'undici'
|
||||
import { Agent, EnvHttpProxyAgent } from 'undici'
|
||||
|
||||
const PROXY_ENV_KEYS = ['HTTP_PROXY', 'http_proxy', 'HTTPS_PROXY', 'https_proxy'] as const
|
||||
|
||||
@ -6,18 +7,30 @@ export function hasProxyEnv(): boolean {
|
||||
return PROXY_ENV_KEYS.some(k => (process.env[k] ?? '') !== '')
|
||||
}
|
||||
|
||||
let resolved = false
|
||||
let agent: EnvHttpProxyAgent | undefined
|
||||
export type ProxyDispatcherOptions = {
|
||||
// --insecure on a self-signed https:// host: skip certificate verification
|
||||
// (local-dev only, same flag that allows plain http:// hosts).
|
||||
readonly insecure?: boolean
|
||||
}
|
||||
|
||||
let resolvedKey: string | undefined
|
||||
let agent: Dispatcher | undefined
|
||||
|
||||
// Node's global fetch ignores HTTP_PROXY / HTTPS_PROXY / NO_PROXY. When a proxy
|
||||
// var is set, route requests through an EnvHttpProxyAgent (it also reads the
|
||||
// lowercase variants and honours NO_PROXY); when none is set, return undefined so
|
||||
// fetch keeps Node's default global dispatcher untouched. Resolved once per
|
||||
// process — proxy env vars are fixed for a single CLI invocation.
|
||||
export function proxyDispatcher(): EnvHttpProxyAgent | undefined {
|
||||
if (!resolved) {
|
||||
agent = hasProxyEnv() ? new EnvHttpProxyAgent() : undefined
|
||||
resolved = true
|
||||
// lowercase variants and honours NO_PROXY); when none is set and TLS verification
|
||||
// isn't being skipped, return undefined so fetch keeps Node's default global
|
||||
// dispatcher untouched. Resolved once per (proxy env, insecure) combination —
|
||||
// both are fixed for a single CLI invocation.
|
||||
export function proxyDispatcher(opts: ProxyDispatcherOptions = {}): Dispatcher | undefined {
|
||||
const insecure = opts.insecure ?? false
|
||||
const key = `${hasProxyEnv()}:${insecure}`
|
||||
if (resolvedKey !== key) {
|
||||
const tls = insecure ? { rejectUnauthorized: false } : undefined
|
||||
agent = hasProxyEnv()
|
||||
? new EnvHttpProxyAgent(tls !== undefined ? { connect: tls, requestTls: tls, proxyTls: tls } : undefined)
|
||||
: (tls !== undefined ? new Agent({ connect: tls }) : undefined)
|
||||
resolvedKey = key
|
||||
}
|
||||
return agent
|
||||
}
|
||||
|
||||
@ -75,6 +75,8 @@ export type ClientOptions = {
|
||||
readonly retryAttempts?: number
|
||||
readonly logger?: HttpLogger
|
||||
readonly hooks?: Hooks
|
||||
// Skip TLS certificate verification (local-dev only, self-signed hosts).
|
||||
readonly insecure?: boolean
|
||||
}
|
||||
|
||||
export type HttpClient = {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { ActiveContext } from '@/auth/hosts'
|
||||
import { BaseError } from '@/errors/base'
|
||||
import { ErrorCode } from '@/errors/codes'
|
||||
|
||||
@ -44,6 +45,16 @@ export function hostWithScheme(host: string, scheme: string | undefined): string
|
||||
return `${proto}://${host}`
|
||||
}
|
||||
|
||||
// Every call site that builds an HTTP client for the active host needs both its
|
||||
// scheme-qualified host and whether TLS verification is disabled for it — derive
|
||||
// them together so neither is forgotten independently.
|
||||
export function activeHostInfo(active: Pick<ActiveContext, 'host' | 'scheme' | 'insecureTls'>): { host: string, insecure: boolean } {
|
||||
return {
|
||||
host: hostWithScheme(active.host, active.scheme),
|
||||
insecure: active.insecureTls === true,
|
||||
}
|
||||
}
|
||||
|
||||
export function bareHost(raw: string): string {
|
||||
try {
|
||||
const u = new URL(raw)
|
||||
|
||||
@ -16,15 +16,18 @@ const UPGRADE_HINT
|
||||
+ '(https://docs.dify.ai/en/getting-started/install-self-hosted)'
|
||||
|
||||
// /_version is unauthenticated; same timeout/no-retry budget as the auto-nudge probe.
|
||||
const defaultProbe: ServerVersionProbe = async (host) => {
|
||||
const http = createHttpClient({ baseURL: openAPIBase(host), timeoutMs: META_PROBE_TIMEOUT_MS, retryAttempts: 0 })
|
||||
return new MetaClient(http).serverVersion()
|
||||
function buildDefaultProbe(insecure: boolean): ServerVersionProbe {
|
||||
return async (host) => {
|
||||
const http = createHttpClient({ baseURL: openAPIBase(host), timeoutMs: META_PROBE_TIMEOUT_MS, retryAttempts: 0, insecure })
|
||||
return new MetaClient(http).serverVersion()
|
||||
}
|
||||
}
|
||||
|
||||
export type EnforceOptions = {
|
||||
readonly probe?: ServerVersionProbe
|
||||
readonly store?: CompatStore
|
||||
readonly forceFresh?: boolean
|
||||
readonly insecure?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,7 +48,7 @@ export async function enforceDifyVersion(
|
||||
if (opts.forceFresh !== true && store.isFreshCompatible(host))
|
||||
return undefined
|
||||
|
||||
const probe = opts.probe ?? defaultProbe
|
||||
const probe = opts.probe ?? buildDefaultProbe(opts.insecure === true)
|
||||
let server: ServerVersionResponse
|
||||
try {
|
||||
server = await probe(host)
|
||||
|
||||
@ -6,7 +6,7 @@ import { META_PROBE_TIMEOUT_MS, MetaClient } from '@/api/meta'
|
||||
import { Registry } from '@/auth/hosts'
|
||||
import { createHttpClient } from '@/http/client'
|
||||
import { arch, platform } from '@/sys/index'
|
||||
import { hostWithScheme, openAPIBase } from '@/util/host'
|
||||
import { activeHostInfo, openAPIBase } from '@/util/host'
|
||||
import { difyCompat, evaluateCompat } from './compat.js'
|
||||
import { versionInfo } from './info.js'
|
||||
|
||||
@ -51,9 +51,11 @@ const defaultLoadActive = async (): Promise<ActiveContext | undefined> => {
|
||||
return (await Registry.load()).resolveActive()
|
||||
}
|
||||
|
||||
const defaultProbe: MetaProbe = async (endpoint) => {
|
||||
const http = createHttpClient({ baseURL: openAPIBase(endpoint), timeoutMs: META_PROBE_TIMEOUT_MS, retryAttempts: 0 })
|
||||
return new MetaClient(http).serverVersion()
|
||||
function buildDefaultProbe(insecure: boolean): MetaProbe {
|
||||
return async (endpoint) => {
|
||||
const http = createHttpClient({ baseURL: openAPIBase(endpoint), timeoutMs: META_PROBE_TIMEOUT_MS, retryAttempts: 0, insecure })
|
||||
return new MetaClient(http).serverVersion()
|
||||
}
|
||||
}
|
||||
|
||||
function buildClientBlock(): ClientBlock {
|
||||
@ -92,7 +94,6 @@ export async function runVersionProbe(opts: RunVersionProbeOptions): Promise<Ver
|
||||
}
|
||||
|
||||
const loadActive = opts.loadActive ?? defaultLoadActive
|
||||
const probe = opts.probe ?? defaultProbe
|
||||
|
||||
let active: ActiveContext | undefined
|
||||
let loadFailed = false
|
||||
@ -112,7 +113,8 @@ export async function runVersionProbe(opts: RunVersionProbeOptions): Promise<Ver
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = hostWithScheme(active.host, active.scheme)
|
||||
const { host: endpoint, insecure } = activeHostInfo(active)
|
||||
const probe = opts.probe ?? buildDefaultProbe(insecure)
|
||||
|
||||
let serverInfo: ServerVersionResponse | undefined
|
||||
try {
|
||||
|
||||
@ -279,11 +279,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/account/(commonLayout)/delete-account/components/check-email.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/account/(commonLayout)/delete-account/components/verify-email.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@ -292,11 +287,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/account/oauth/authorize/layout.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/account/oauth/authorize/page.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -3406,30 +3396,11 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx": {
|
||||
"jsx-a11y/no-autofocus": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/members-page/invite-modal/index.tsx": {
|
||||
"jsx-a11y/no-autofocus": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
@ -3571,14 +3542,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 2
|
||||
|
||||
@ -6,11 +6,32 @@ import AccountDropdown from '@/app/components/header/account-dropdown'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
|
||||
const {
|
||||
mockAppContextState,
|
||||
mockPush,
|
||||
mockLogout,
|
||||
mockResetUser,
|
||||
mockSetShowAccountSettingModal,
|
||||
} = vi.hoisted(() => ({
|
||||
mockAppContextState: {
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
name: 'Ada Lovelace',
|
||||
email: 'ada@example.com',
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
is_password_set: true,
|
||||
},
|
||||
langGeniusVersionInfo: {
|
||||
current_env: 'CLOUD',
|
||||
current_version: '1.0.0',
|
||||
latest_version: '1.1.0',
|
||||
release_date: '',
|
||||
release_notes: 'https://example.com/releases/1.1.0',
|
||||
version: '1.0.0',
|
||||
can_auto_update: false,
|
||||
},
|
||||
isCurrentWorkspaceOwner: false,
|
||||
},
|
||||
mockPush: vi.fn(),
|
||||
mockLogout: vi.fn(),
|
||||
mockResetUser: vi.fn(),
|
||||
@ -28,21 +49,19 @@ vi.mock('react-i18next', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
userProfile: {
|
||||
name: 'Ada Lovelace',
|
||||
email: 'ada@example.com',
|
||||
avatar_url: '',
|
||||
},
|
||||
langGeniusVersionInfo: {
|
||||
current_version: '1.0.0',
|
||||
latest_version: '1.1.0',
|
||||
release_notes: 'https://example.com/releases/1.1.0',
|
||||
},
|
||||
isCurrentWorkspaceOwner: false,
|
||||
}),
|
||||
useAppContext: () => mockAppContextState,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
isEducationAccount: false,
|
||||
@ -62,7 +81,8 @@ vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path: string) => `https://docs.example.com${path}`,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
vi.mock('@/service/use-common', async importOriginal => ({
|
||||
...await importOriginal<typeof import('@/service/use-common')>(),
|
||||
useLogout: () => ({
|
||||
mutateAsync: mockLogout,
|
||||
}),
|
||||
|
||||
@ -13,6 +13,7 @@ export type AppContextStateMockState = {
|
||||
} | null
|
||||
currentWorkspace?: {
|
||||
id?: string
|
||||
name?: string
|
||||
} | null
|
||||
isCurrentWorkspaceManager?: boolean
|
||||
isCurrentWorkspaceOwner?: boolean
|
||||
@ -22,6 +23,8 @@ export type AppContextStateMockState = {
|
||||
isLoadingWorkspacePermissionKeys?: boolean
|
||||
workspacePermissionKeys?: string[]
|
||||
langGeniusVersionInfo?: LangGeniusVersionResponse
|
||||
refreshUserProfile?: () => void
|
||||
refreshCurrentWorkspace?: () => void
|
||||
}
|
||||
|
||||
type AppContextStateAtomKind
|
||||
@ -32,11 +35,14 @@ type AppContextStateAtomKind
|
||||
| 'currentWorkspaceId'
|
||||
| 'workspaceRoleFlags'
|
||||
| 'isCurrentWorkspaceManager'
|
||||
| 'isCurrentWorkspaceOwner'
|
||||
| 'currentWorkspaceLoading'
|
||||
| 'workspacePermissionKeys'
|
||||
| 'workspacePermissionKeysLoading'
|
||||
| 'langGeniusVersionInfo'
|
||||
| 'langGeniusCurrentVersion'
|
||||
| 'refreshUserProfile'
|
||||
| 'refreshCurrentWorkspace'
|
||||
|
||||
type AppContextStateMockAtom = {
|
||||
[APP_CONTEXT_STATE_ATOM_KIND]: AppContextStateAtomKind
|
||||
@ -57,6 +63,7 @@ const defaultUserProfile = {
|
||||
|
||||
const defaultCurrentWorkspace = {
|
||||
id: 'workspace-1',
|
||||
name: 'Workspace',
|
||||
}
|
||||
|
||||
const defaultLangGeniusVersionInfo = {
|
||||
@ -109,11 +116,14 @@ export const createAppContextStateAtomMock = async (
|
||||
currentWorkspaceIdAtom: createMockAtom('currentWorkspaceId'),
|
||||
workspaceRoleFlagsAtom: createMockAtom('workspaceRoleFlags'),
|
||||
isCurrentWorkspaceManagerAtom: createMockAtom('isCurrentWorkspaceManager'),
|
||||
isCurrentWorkspaceOwnerAtom: createMockAtom('isCurrentWorkspaceOwner'),
|
||||
currentWorkspaceLoadingAtom: createMockAtom('currentWorkspaceLoading'),
|
||||
workspacePermissionKeysAtom: createMockAtom('workspacePermissionKeys'),
|
||||
workspacePermissionKeysLoadingAtom: createMockAtom('workspacePermissionKeysLoading'),
|
||||
langGeniusVersionInfoAtom: createMockAtom('langGeniusVersionInfo'),
|
||||
langGeniusCurrentVersionAtom: createMockAtom('langGeniusCurrentVersion'),
|
||||
refreshUserProfileAtom: createMockAtom('refreshUserProfile'),
|
||||
refreshCurrentWorkspaceAtom: createMockAtom('refreshCurrentWorkspace'),
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,6 +172,9 @@ export const createAppContextStateJotaiMock = async (
|
||||
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'isCurrentWorkspaceManager')
|
||||
return state.isCurrentWorkspaceManager ?? false
|
||||
|
||||
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'isCurrentWorkspaceOwner')
|
||||
return state.isCurrentWorkspaceOwner ?? false
|
||||
|
||||
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'currentWorkspaceLoading')
|
||||
return state.isLoadingCurrentWorkspace ?? false
|
||||
|
||||
@ -179,5 +192,22 @@ export const createAppContextStateJotaiMock = async (
|
||||
|
||||
throw new Error(`Unsupported app context state atom: ${atom[APP_CONTEXT_STATE_ATOM_KIND]}`)
|
||||
},
|
||||
useSetAtom: (atom: unknown) => {
|
||||
if (!isAppContextStateMockAtom(atom))
|
||||
return actual.useSetAtom(atom as Parameters<typeof actual.useSetAtom>[0])
|
||||
|
||||
if (!appContextStateMockRegistry)
|
||||
throw new Error('App context state atom mock is not initialized')
|
||||
|
||||
const state = appContextStateMockRegistry.getState()
|
||||
|
||||
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'refreshUserProfile')
|
||||
return state.refreshUserProfile ?? (() => {})
|
||||
|
||||
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'refreshCurrentWorkspace')
|
||||
return state.refreshCurrentWorkspace ?? (() => {})
|
||||
|
||||
throw new Error(`Unsupported app context state write atom: ${atom[APP_CONTEXT_STATE_ATOM_KIND]}`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileEmailAtom } from '@/context/app-context-state'
|
||||
import Link from '@/next/link'
|
||||
import { useSendDeleteAccountEmail } from '../state'
|
||||
|
||||
@ -14,7 +15,7 @@ type DeleteAccountProps = {
|
||||
|
||||
export default function CheckEmail(props: DeleteAccountProps) {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile } = useAppContext()
|
||||
const userProfileEmail = useAtomValue(userProfileEmailAtom)
|
||||
const [userInputEmail, setUserInputEmail] = useState('')
|
||||
|
||||
const { isPending: isSendingEmail, mutateAsync: getDeleteEmailVerifyCode } = useSendDeleteAccountEmail()
|
||||
@ -45,7 +46,7 @@ export default function CheckEmail(props: DeleteAccountProps) {
|
||||
}}
|
||||
/>
|
||||
<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" disabled={userInputEmail !== userProfileEmail || 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>
|
||||
</>
|
||||
|
||||
@ -3,9 +3,10 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileEmailAtom } from '@/context/app-context-state'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { useDeleteAccountFeedback } from '../state'
|
||||
@ -17,7 +18,7 @@ type DeleteAccountProps = {
|
||||
|
||||
export default function FeedBack(props: DeleteAccountProps) {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile } = useAppContext()
|
||||
const userProfileEmail = useAtomValue(userProfileEmailAtom)
|
||||
const router = useRouter()
|
||||
const [userFeedback, setUserFeedback] = useState('')
|
||||
const { isPending, mutateAsync: sendFeedback } = useDeleteAccountFeedback()
|
||||
@ -35,12 +36,12 @@ export default function FeedBack(props: DeleteAccountProps) {
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
try {
|
||||
await sendFeedback({ feedback: userFeedback, email: userProfile.email })
|
||||
await sendFeedback({ feedback: userFeedback, email: userProfileEmail })
|
||||
props.onConfirm()
|
||||
await handleSuccess()
|
||||
}
|
||||
catch (error) { console.error(error) }
|
||||
}, [handleSuccess, userFeedback, sendFeedback, userProfile, props])
|
||||
}, [handleSuccess, userFeedback, sendFeedback, userProfileEmail, props])
|
||||
|
||||
const handleSkip = useCallback(() => {
|
||||
props.onCancel()
|
||||
|
||||
@ -1,60 +1,40 @@
|
||||
'use client'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Header from '@/app/signin/_header'
|
||||
import { AppContextProvider } from '@/context/app-context-provider'
|
||||
import { isLegacyBase401, userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
|
||||
export default function SignInLayout({ children }: any) {
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const copyrightYear = new Date().getFullYear()
|
||||
|
||||
export default function OAuthAuthorizeLayout({ children }: Props) {
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
useDocumentTitle('')
|
||||
// Probe login state. 401 stays as `error` (not thrown) so this layout can render
|
||||
// the signin/oauth UI for unauthenticated users; other errors bubble to error.tsx.
|
||||
// (When unauthenticated, service/base.ts's auto-redirect to /signin still fires.)
|
||||
const { isPending, data: userResp, error } = useQuery({
|
||||
...userProfileQueryOptions(),
|
||||
throwOnError: err => !isLegacyBase401(err),
|
||||
})
|
||||
const isLoggedIn = !!userResp && !error
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full justify-center bg-background-default-burn">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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 items-center 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 flex-col md:w-[400px]">
|
||||
{isLoggedIn
|
||||
? (
|
||||
<AppContextProvider>
|
||||
{children}
|
||||
</AppContextProvider>
|
||||
)
|
||||
: children}
|
||||
</div>
|
||||
<div className="flex min-h-screen w-full justify-center bg-background-default-burn p-6">
|
||||
<div className="flex w-full shrink-0 flex-col items-center rounded-2xl border border-effects-highlight bg-background-default-subtle">
|
||||
<Header />
|
||||
<div className="flex w-full grow flex-col items-center justify-center px-6 md:px-[108px]">
|
||||
<div className="flex flex-col md:w-[400px]">
|
||||
{children}
|
||||
</div>
|
||||
{systemFeatures.branding.enabled === false && (
|
||||
<div className="px-8 py-6 system-xs-regular text-text-tertiary">
|
||||
©
|
||||
{' '}
|
||||
{new Date().getFullYear()}
|
||||
{' '}
|
||||
LangGenius, Inc. All rights reserved.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{systemFeatures.branding.enabled === false && (
|
||||
<div className="px-8 py-6 system-xs-regular text-text-tertiary">
|
||||
©
|
||||
{' '}
|
||||
{copyrightYear}
|
||||
{' '}
|
||||
LangGenius, Inc. All rights reserved.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -43,6 +43,9 @@ vi.mock('@/app/components/base/theme-switcher', () => ({
|
||||
const { mockSetTheme } = vi.hoisted(() => ({
|
||||
mockSetTheme: vi.fn(),
|
||||
}))
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
current: undefined as AppContextValue | undefined,
|
||||
}))
|
||||
|
||||
vi.mock('next-themes', () => ({
|
||||
useTheme: () => ({
|
||||
@ -55,6 +58,16 @@ vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current ?? {})
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: vi.fn(),
|
||||
}))
|
||||
@ -63,7 +76,8 @@ vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
vi.mock('@/service/use-common', async importOriginal => ({
|
||||
...await importOriginal<typeof import('@/service/use-common')>(),
|
||||
useLogout: vi.fn(),
|
||||
}))
|
||||
|
||||
@ -150,6 +164,11 @@ const baseAppContextValue: AppContextValue = {
|
||||
workspacePermissionKeys: [],
|
||||
}
|
||||
|
||||
const setAppContextValue = (value: AppContextValue) => {
|
||||
mockAppContextState.current = value
|
||||
vi.mocked(useAppContext).mockReturnValue(value)
|
||||
}
|
||||
|
||||
describe('AccountDropdown', () => {
|
||||
const mockPush = vi.fn()
|
||||
const mockLogout = vi.fn()
|
||||
@ -170,7 +189,7 @@ describe('AccountDropdown', () => {
|
||||
mockConfig.IS_CLOUD_EDITION = false
|
||||
mockEnv.env.NEXT_PUBLIC_SITE_ABOUT = 'show'
|
||||
|
||||
vi.mocked(useAppContext).mockReturnValue(baseAppContextValue)
|
||||
setAppContextValue(baseAppContextValue)
|
||||
vi.mocked(useProviderContext).mockReturnValue({
|
||||
isEducationAccount: false,
|
||||
plan: { type: Plan.sandbox },
|
||||
@ -267,7 +286,7 @@ describe('AccountDropdown', () => {
|
||||
it('should show Compliance in Cloud Edition for workspace owner', () => {
|
||||
// Arrange
|
||||
mockConfig.IS_CLOUD_EDITION = true
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
setAppContextValue({
|
||||
...baseAppContextValue,
|
||||
userProfile: { ...baseAppContextValue.userProfile, name: 'User' },
|
||||
isCurrentWorkspaceOwner: true,
|
||||
@ -286,7 +305,7 @@ describe('AccountDropdown', () => {
|
||||
it('should hide Compliance in Cloud Edition when user is not workspace owner', () => {
|
||||
// Arrange
|
||||
mockConfig.IS_CLOUD_EDITION = true
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
setAppContextValue({
|
||||
...baseAppContextValue,
|
||||
isCurrentWorkspaceOwner: false,
|
||||
})
|
||||
@ -382,7 +401,7 @@ describe('AccountDropdown', () => {
|
||||
describe('Version Indicators', () => {
|
||||
it('should show orange indicator when version is not latest', () => {
|
||||
// Arrange
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
setAppContextValue({
|
||||
...baseAppContextValue,
|
||||
userProfile: { ...baseAppContextValue.userProfile, name: 'User' },
|
||||
langGeniusVersionInfo: {
|
||||
@ -402,7 +421,7 @@ describe('AccountDropdown', () => {
|
||||
|
||||
it('should show green indicator when version is latest', () => {
|
||||
// Arrange
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
setAppContextValue({
|
||||
...baseAppContextValue,
|
||||
userProfile: { ...baseAppContextValue.userProfile, name: 'User' },
|
||||
langGeniusVersionInfo: {
|
||||
|
||||
@ -10,12 +10,13 @@ import {
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import ThemeSwitcher from '@/app/components/base/theme-switcher'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { isCurrentWorkspaceOwnerAtom, langGeniusVersionInfoAtom, userProfileAtom } from '@/context/app-context-state'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
@ -119,7 +120,9 @@ export function DefaultMenuContent({
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { userProfile, langGeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
|
||||
const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom)
|
||||
const { isEducationAccount } = useProviderContext()
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
|
||||
|
||||
@ -8,11 +8,12 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||
import { useSetEducationExpiredHasNoticed, useSetEducationReverifyHasNoticed, useSetEducationReverifyPrevExpireAt } from '@/app/education-apply/storage'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { langGeniusVersionInfoAtom, userProfileAtom } from '@/context/app-context-state'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import AccountAbout from '../account-about'
|
||||
@ -37,7 +38,8 @@ export default function AppSelector({
|
||||
const [aboutVisible, setAboutVisible] = useState(false)
|
||||
const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const { userProfile, langGeniusVersionInfo } = useAppContext()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
|
||||
const clearEducationReverifyPrevExpireAt = useSetEducationReverifyPrevExpireAt()
|
||||
const clearEducationReverifyHasNoticed = useSetEducationReverifyHasNoticed()
|
||||
const clearEducationExpiredHasNoticed = useSetEducationExpiredHasNoticed()
|
||||
|
||||
@ -16,11 +16,12 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileAtom } from '@/context/app-context-state'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import Link from '@/next/link'
|
||||
@ -87,7 +88,7 @@ export function MainNavMenuContent({
|
||||
onLogout,
|
||||
}: MainNavMenuContentProps) {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile } = useAppContext()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const { isEducationAccount } = useProviderContext()
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
|
||||
|
||||
@ -15,6 +15,18 @@ vi.mock('@/context/app-context', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
workspacePermissionKeys: mocks.workspacePermissionKeys,
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
const rule: AccessPolicyWithBindings = {
|
||||
policy: {
|
||||
id: 'app-rule-1',
|
||||
|
||||
@ -3,11 +3,12 @@
|
||||
import type { AccessPolicyWithBindings } from '@/models/access-control'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import AccessRuleRow from './access-rule-row'
|
||||
|
||||
@ -46,7 +47,7 @@ const AccessRuleSection = ({
|
||||
const [expanded, setExpanded] = useState(defaultExpanded)
|
||||
const listRef = useRef<HTMLDivElement>(null)
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const workspacePermissionKeys = useAppContextWithSelector(s => s.workspacePermissionKeys)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const canManage = hasPermission(workspacePermissionKeys, 'workspace.role.manage')
|
||||
const ruleCount = totalCount ?? rules.length
|
||||
|
||||
|
||||
@ -24,6 +24,18 @@ vi.mock('@/context/app-context', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
workspacePermissionKeys: mockWorkspacePermissionKeys.current,
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
apiBasedExtension: {
|
||||
|
||||
@ -2,11 +2,12 @@ import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-
|
||||
import type { ReactNode } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SearchInput } from '@/app/components/base/search-input'
|
||||
import { SkeletonContainer, SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import { Empty } from './empty'
|
||||
@ -51,7 +52,7 @@ export function ApiBasedExtensionPage({
|
||||
layout,
|
||||
}: ApiBasedExtensionPageProps = {}) {
|
||||
const { t } = useTranslation()
|
||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const canManage = hasPermission(workspacePermissionKeys, 'api_extension.manage')
|
||||
const { data: apiBasedExtensions = [], isPending: isLoading } = useQuery(consoleQuery.apiBasedExtension.get.queryOptions())
|
||||
const [dialogState, setDialogState] = useState<ApiBasedExtensionDialogState>(null)
|
||||
|
||||
@ -4,6 +4,7 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BillingPage from '@/app/components/billing/billing-page'
|
||||
@ -12,7 +13,7 @@ import {
|
||||
ACCOUNT_SETTING_TAB,
|
||||
} from '@/app/components/header/account-setting/constants'
|
||||
import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
@ -54,7 +55,7 @@ export default function AccountSetting({
|
||||
const { t } = useTranslation()
|
||||
const { enableBilling, enableReplaceWebAppLogo } = useProviderContext()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const { workspacePermissionKeys } = useAppContext()
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const isRbacEnabled = systemFeatures.rbac_enabled
|
||||
const canManageWorkspaceRoles = isRbacEnabled && hasPermission(workspacePermissionKeys, 'workspace.role.manage')
|
||||
const canViewBilling = enableBilling && hasPermission(workspacePermissionKeys, BillingPermission.View)
|
||||
|
||||
@ -14,7 +14,19 @@ import { useUpdateRolesOfMember } from '@/service/access-control/use-member-role
|
||||
import { useMembers } from '@/service/use-common'
|
||||
import MembersPage from '../index'
|
||||
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
current: {} as Partial<AppContextValue>,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context')
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
|
||||
})
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
vi.mock('@/context/provider-context')
|
||||
vi.mock('@/hooks/use-format-time-from-now')
|
||||
vi.mock('@/service/access-control/use-member-roles')
|
||||
@ -41,6 +53,11 @@ const createRole = (overrides: Partial<Role>): Role => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const setAppContextValue = (value: AppContextValue) => {
|
||||
mockAppContextState.current = value
|
||||
vi.mocked(useAppContext).mockReturnValue(value)
|
||||
}
|
||||
|
||||
vi.mock('../edit-workspace-modal', () => ({
|
||||
default: ({ onCancel }: { onCancel: () => void }) => (
|
||||
<div>
|
||||
@ -181,7 +198,7 @@ describe('MembersPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
setAppContextValue({
|
||||
userProfile: { email: 'owner@example.com' },
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'owner' } as ICurrentWorkspace,
|
||||
isCurrentWorkspaceOwner: true,
|
||||
@ -289,7 +306,7 @@ describe('MembersPage', () => {
|
||||
})
|
||||
|
||||
it('should hide manager controls for non-owner non-manager users', () => {
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
setAppContextValue({
|
||||
userProfile: { email: 'admin@example.com' },
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||
isCurrentWorkspaceOwner: false,
|
||||
@ -390,7 +407,7 @@ describe('MembersPage', () => {
|
||||
})
|
||||
|
||||
it('should show invite button when user is manager but not owner', () => {
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
setAppContextValue({
|
||||
userProfile: { email: 'admin@example.com' },
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||
isCurrentWorkspaceOwner: false,
|
||||
@ -405,7 +422,7 @@ describe('MembersPage', () => {
|
||||
})
|
||||
|
||||
it('should allow admins to operate other non-owner members only', () => {
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
setAppContextValue({
|
||||
userProfile: { email: 'admin@example.com' },
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||
isCurrentWorkspaceOwner: false,
|
||||
@ -483,7 +500,7 @@ describe('MembersPage', () => {
|
||||
})
|
||||
|
||||
it('should render role badge names from account roles', () => {
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
setAppContextValue({
|
||||
userProfile: { email: 'admin@example.com' },
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||
isCurrentWorkspaceOwner: false,
|
||||
@ -551,7 +568,7 @@ describe('MembersPage', () => {
|
||||
|
||||
it('should not allow assigning roles from member details when target is current user', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
setAppContextValue({
|
||||
userProfile: { email: 'admin@example.com' },
|
||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||
isCurrentWorkspaceOwner: false,
|
||||
|
||||
@ -8,6 +8,16 @@ import { useWorkspacePermissions } from '@/service/use-workspace'
|
||||
import InviteButton from '../invite-button'
|
||||
|
||||
vi.mock('@/context/app-context')
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
currentWorkspace: { id: 'workspace-id' },
|
||||
}))
|
||||
})
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
vi.mock('@/service/use-workspace')
|
||||
|
||||
describe('InviteButton', () => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { AppContextValue } from '@/context/app-context'
|
||||
import { render } from '@testing-library/react'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import EditWorkspaceModal from '../index'
|
||||
@ -10,6 +11,9 @@ type DialogProps = {
|
||||
}
|
||||
|
||||
let latestOnOpenChange: DialogProps['onOpenChange']
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
current: {} as Partial<AppContextValue>,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dialog', () => ({
|
||||
Dialog: ({ children, onOpenChange }: DialogProps) => {
|
||||
@ -29,14 +33,26 @@ vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
describe('EditWorkspaceModal dialog lifecycle', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
latestOnOpenChange = undefined
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
const appContextValue = {
|
||||
currentWorkspace: { name: 'Test Workspace' },
|
||||
isCurrentWorkspaceOwner: true,
|
||||
} as never)
|
||||
} as never
|
||||
mockAppContextState.current = appContextValue
|
||||
vi.mocked(useAppContext).mockReturnValue(appContextValue)
|
||||
})
|
||||
|
||||
it('should only call onCancel when the dialog requests closing', () => {
|
||||
|
||||
@ -10,10 +10,21 @@ import EditWorkspaceModal from '../index'
|
||||
const toastMocks = vi.hoisted(() => ({
|
||||
mockNotify: vi.fn(),
|
||||
}))
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
current: {} as Partial<AppContextValue>,
|
||||
}))
|
||||
|
||||
const getSaveButton = () => screen.getByRole('button', { name: /operation\.(save|saving)/i })
|
||||
|
||||
vi.mock('@/context/app-context')
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
|
||||
})
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
vi.mock('@/service/common')
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
default: {
|
||||
@ -34,10 +45,12 @@ describe('EditWorkspaceModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
const appContextValue = {
|
||||
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
|
||||
isCurrentWorkspaceOwner: true,
|
||||
} as unknown as AppContextValue)
|
||||
} as unknown as AppContextValue
|
||||
mockAppContextState.current = appContextValue
|
||||
vi.mocked(useAppContext).mockReturnValue(appContextValue)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@ -2,11 +2,12 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useId, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom } from '@/context/app-context-state'
|
||||
import { updateWorkspaceInfo } from '@/service/common'
|
||||
|
||||
type IEditWorkspaceModalProps = {
|
||||
@ -14,7 +15,8 @@ type IEditWorkspaceModalProps = {
|
||||
}
|
||||
const EditWorkspaceModal = ({ onCancel }: IEditWorkspaceModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext()
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom)
|
||||
const [name, setName] = useState<string>(currentWorkspace.name)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const inputId = useId()
|
||||
@ -82,7 +84,6 @@ const EditWorkspaceModal = ({ onCancel }: IEditWorkspaceModalProps) => {
|
||||
</label>
|
||||
<Input
|
||||
id={inputId}
|
||||
autoFocus
|
||||
value={name}
|
||||
placeholder={t('account.workspaceNamePlaceholder', { ns: 'common' })}
|
||||
onChange={(e) => {
|
||||
|
||||
@ -4,12 +4,13 @@ import type { InvitationResult, Member } from '@/models/common'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NUM_INFINITE } from '@/app/components/billing/config'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom, userProfileEmailAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
@ -30,7 +31,10 @@ const MembersPage = () => {
|
||||
const locale = useLocale()
|
||||
const language = getAccessControlTemplateLanguage(locale)
|
||||
|
||||
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, workspacePermissionKeys } = useAppContext()
|
||||
const userProfileEmail = useAtomValue(userProfileEmailAtom)
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const { data, refetch } = useMembers(language)
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false)
|
||||
@ -162,7 +166,7 @@ const MembersPage = () => {
|
||||
key={account.id}
|
||||
member={account}
|
||||
roles={account.roles}
|
||||
isCurrentUser={userProfile.email === account.email}
|
||||
isCurrentUser={userProfileEmail === account.email}
|
||||
canManage={canManageMembers}
|
||||
canTransferOwnership={isCurrentWorkspaceOwner && isAllowTransferWorkspace}
|
||||
allowMultipleRoles={systemFeatures.rbac_enabled}
|
||||
@ -213,7 +217,7 @@ const MembersPage = () => {
|
||||
canAssignRoles={
|
||||
canManageMembers
|
||||
&& detailsMember.role !== 'owner'
|
||||
&& userProfile.email !== detailsMember.email
|
||||
&& userProfileEmail !== detailsMember.email
|
||||
}
|
||||
allowMultipleRoles={systemFeatures.rbac_enabled}
|
||||
onClose={handleCloseDetails}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { RiUserAddLine } from '@remixicon/react'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { currentWorkspaceIdAtom } from '@/context/app-context-state'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useWorkspacePermissions } from '@/service/use-workspace'
|
||||
|
||||
@ -14,9 +15,9 @@ type InviteButtonProps = {
|
||||
|
||||
const InviteButton = (props: InviteButtonProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { currentWorkspace } = useAppContext()
|
||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom)
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const { data: workspacePermissions, isFetching: isFetchingWorkspacePermissions } = useWorkspacePermissions(currentWorkspace!.id, systemFeatures.branding.enabled)
|
||||
const { data: workspacePermissions, isFetching: isFetchingWorkspacePermissions } = useWorkspacePermissions(currentWorkspaceId, systemFeatures.branding.enabled)
|
||||
if (systemFeatures.branding.enabled) {
|
||||
if (isFetchingWorkspacePermissions) {
|
||||
return <Loading />
|
||||
|
||||
@ -11,8 +11,19 @@ import TransferOwnershipModal from '../index'
|
||||
const toastMocks = vi.hoisted(() => ({
|
||||
mockNotify: vi.fn(),
|
||||
}))
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
current: {} as Partial<AppContextValue>,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context')
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
|
||||
})
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
vi.mock('@/service/common')
|
||||
vi.mock('@/service/use-common')
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
@ -40,10 +51,12 @@ describe('TransferOwnershipModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
vi.mocked(useAppContext).mockReturnValue({
|
||||
const appContextValue = {
|
||||
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
|
||||
userProfile: { email: 'owner@example.com', id: 'owner-id' },
|
||||
} as unknown as AppContextValue)
|
||||
} as unknown as AppContextValue
|
||||
mockAppContextState.current = appContextValue
|
||||
vi.mocked(useAppContext).mockReturnValue(appContextValue)
|
||||
|
||||
vi.mocked(useMembers).mockReturnValue({
|
||||
data: { accounts: [] },
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { currentWorkspaceAtom, userProfileAtom } from '@/context/app-context-state'
|
||||
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
|
||||
import MemberSelector from './member-selector'
|
||||
|
||||
@ -13,15 +14,22 @@ type Props = Readonly<{
|
||||
show: boolean
|
||||
onClose: () => void
|
||||
}>
|
||||
enum STEP {
|
||||
start = 'start',
|
||||
verify = 'verify',
|
||||
transfer = 'transfer',
|
||||
const STEP = {
|
||||
start: 'start',
|
||||
verify: 'verify',
|
||||
transfer: 'transfer',
|
||||
}
|
||||
type Step = typeof STEP[keyof typeof STEP]
|
||||
|
||||
const getErrorMessage = (error: unknown) => {
|
||||
return error instanceof Error ? error.message : ''
|
||||
}
|
||||
|
||||
const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { currentWorkspace, userProfile } = useAppContext()
|
||||
const [step, setStep] = useState<STEP>(STEP.start)
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const [step, setStep] = useState<Step>(STEP.start)
|
||||
const [code, setCode] = useState<string>('')
|
||||
const [time, setTime] = useState<number>(0)
|
||||
const [stepToken, setStepToken] = useState<string>('')
|
||||
@ -71,7 +79,7 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
toast.error(`Error verifying email: ${error ? (error as any).message : ''}`)
|
||||
toast.error(`Error verifying email: ${getErrorMessage(error)}`)
|
||||
}
|
||||
}
|
||||
const sendCodeToOriginEmail = async () => {
|
||||
@ -96,7 +104,7 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
||||
globalThis.location.reload()
|
||||
}
|
||||
catch (error) {
|
||||
toast.error(`Error ownership transfer: ${error ? (error as any).message : ''}`)
|
||||
toast.error(`Error ownership transfer: ${getErrorMessage(error)}`)
|
||||
}
|
||||
finally {
|
||||
setIsTransfer(false)
|
||||
|
||||
@ -22,6 +22,18 @@ vi.mock('@/context/app-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
workspacePermissionKeys: mockWorkspacePermissionKeys,
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
plan: { type: mockPlanType },
|
||||
|
||||
@ -11,6 +11,18 @@ vi.mock('@/context/app-context', () => ({
|
||||
selector({ workspacePermissionKeys: mockWorkspacePermissionKeys }),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
workspacePermissionKeys: mockWorkspacePermissionKeys,
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContextSelector: (selector: (state: { setShowModelLoadBalancingModal: typeof mockSetShowModelLoadBalancingModal }) => unknown) =>
|
||||
selector({ setShowModelLoadBalancingModal: mockSetShowModelLoadBalancingModal }),
|
||||
|
||||
@ -7,6 +7,7 @@ import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@ -14,7 +15,7 @@ import {
|
||||
ManageCustomModelCredentials,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import { useCredentialPermissions } from '@/hooks/use-credential-permissions'
|
||||
import { renderI18nObject } from '@/i18n-config'
|
||||
@ -68,7 +69,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
}))
|
||||
const hasModelList = hasFetchedModelList && !!modelList.length
|
||||
const showCollapsedSection = !expanded || !hasFetchedModelList
|
||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const showModelProvider = systemConfig.enabled && MODEL_PROVIDER_QUOTA_GET_PAID.includes(currentProviderName as ModelProviderQuotaGetPaid) && !IS_CE_EDITION
|
||||
const canConfigureModels = hasPermission(workspacePermissionKeys, 'plugin.model_config')
|
||||
const { canUseCredential, canCreateCredential, canManageCredential } = useCredentialPermissions()
|
||||
|
||||
@ -4,12 +4,13 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||
import { useProviderContext, useProviderContextSelector } from '@/context/provider-context'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { disableModel, enableModel } from '@/service/common'
|
||||
@ -32,7 +33,7 @@ const ModelListItem = ({ model, provider, isConfigurable, onChange, onModifyLoad
|
||||
const { t } = useTranslation()
|
||||
const { plan } = useProviderContext()
|
||||
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
|
||||
const { workspacePermissionKeys } = useAppContext()
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const canConfigureModels = hasPermission(workspacePermissionKeys, 'plugin.model_config')
|
||||
const queryClient = useQueryClient()
|
||||
const updateModelList = useUpdateModelList()
|
||||
|
||||
@ -4,13 +4,14 @@ import type {
|
||||
ModelItem,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
AddCustomModel,
|
||||
ManageCustomModelCredentials,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import {
|
||||
@ -33,7 +34,7 @@ const ModelList: FC<ModelListProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const configurativeMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
|
||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const canConfigureModels = hasPermission(workspacePermissionKeys, 'plugin.model_config')
|
||||
const isConfigurable = configurativeMethods.includes(ConfigurationMethodEnum.customizableModel)
|
||||
const setShowModelLoadBalancingModal = useModalContextSelector(state => state.setShowModelLoadBalancingModal)
|
||||
@ -58,13 +59,14 @@ const ModelList: FC<ModelListProps> = ({
|
||||
{t('modelProvider.modelsNum', { ns: 'common', num: models.length })}
|
||||
<span className="mr-0.5 i-ri-arrow-right-s-line size-4 rotate-90" />
|
||||
</span>
|
||||
<span
|
||||
className="hidden h-6 cursor-pointer items-center rounded-lg bg-state-base-hover pr-1.5 pl-1 system-xs-medium text-text-tertiary group-hover:inline-flex"
|
||||
<button
|
||||
type="button"
|
||||
className="hidden h-6 cursor-pointer items-center rounded-lg border-none bg-state-base-hover pr-1.5 pl-1 system-xs-medium text-text-tertiary group-hover:inline-flex"
|
||||
onClick={() => onCollapse()}
|
||||
>
|
||||
{t('modelProvider.modelsNum', { ns: 'common', num: models.length })}
|
||||
<span className="mr-0.5 i-ri-arrow-right-s-line size-4 rotate-90" />
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
{
|
||||
isConfigurable && canConfigureModels && (
|
||||
|
||||
@ -40,6 +40,18 @@ vi.mock('@/context/app-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
workspacePermissionKeys: mockWorkspacePermissionKeys,
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
textGenerationModelList: [],
|
||||
|
||||
@ -12,10 +12,11 @@ import {
|
||||
DialogTitle,
|
||||
} from '@langgenius/dify-ui/dialog'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { updateDefaultModel } from '@/service/common'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
@ -68,7 +69,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
|
||||
onOpenMarketplace,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { workspacePermissionKeys } = useAppContext()
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
const canManageSystemDefaultModel = hasPermission(workspacePermissionKeys, 'plugin.model_config')
|
||||
const updateModelList = useUpdateModelList()
|
||||
|
||||
@ -26,6 +26,18 @@ vi.mock('@/context/app-context', () => ({
|
||||
} as AppContextValue)),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
workspacePermissionKeys: mocks.workspacePermissionKeys,
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/service/access-control/use-workspace-roles', () => ({
|
||||
useCreateWorkspaceRole: vi.fn(),
|
||||
useUpdateWorkspaceRole: vi.fn(),
|
||||
|
||||
@ -4,9 +4,10 @@ import type { RoleModalMode, submitRoleData } from './role-modal'
|
||||
import type { Role } from '@/models/access-control'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { getAccessControlTemplateLanguage } from '@/i18n-config/language'
|
||||
import { useCreateWorkspaceRole, useUpdateWorkspaceRole } from '@/service/access-control/use-workspace-roles'
|
||||
@ -32,7 +33,7 @@ const PermissionsPage = ({ containerRef }: PermissionsPageProps) => {
|
||||
const [modalState, setModalState] = useState<ModalState>(null)
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const workspacePermissionKeys = useAppContextWithSelector(s => s.workspacePermissionKeys)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
|
||||
const language = useMemo(() => getAccessControlTemplateLanguage(locale), [locale])
|
||||
|
||||
|
||||
@ -36,6 +36,18 @@ vi.mock('@/context/app-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
workspacePermissionKeys: mockWorkspacePermissionKeys.value,
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/service/access-control/use-workspace-roles', () => ({
|
||||
useCopyWorkspaceRole: () => ({
|
||||
mutate: mockCopyRole,
|
||||
|
||||
@ -18,10 +18,11 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||
import { useCopyWorkspaceRole, useDeleteWorkspaceRole } from '@/service/access-control/use-workspace-roles'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import { CopyMembersConfirmDialog } from './copy-members-confirm-dialog'
|
||||
@ -44,7 +45,7 @@ const RowMenu = ({
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const [showCopyMembersConfirm, setShowCopyMembersConfirm] = useState(false)
|
||||
|
||||
const workspacePermissionKeys = useAppContextWithSelector(s => s.workspacePermissionKeys)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const canManageRoles = hasPermission(workspacePermissionKeys, 'workspace.role.manage')
|
||||
|
||||
const handleView = useCallback(() => onView?.(role), [onView, role])
|
||||
|
||||
@ -75,6 +75,19 @@ vi.mock('@/context/app-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
userProfile: mockUserProfile,
|
||||
refreshUserProfile: mockMutateUserProfile,
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: () => mockLocale,
|
||||
}))
|
||||
|
||||
@ -2,10 +2,11 @@
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { refreshUserProfileAtom, userProfileAtom } from '@/context/app-context-state'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
@ -35,7 +36,8 @@ const isThemeOption = (value: string): value is ThemeOption => {
|
||||
|
||||
export default function PreferencePage() {
|
||||
const locale = useLocale()
|
||||
const { userProfile, mutateUserProfile } = useAppContext()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const refreshUserProfile = useSetAtom(refreshUserProfileAtom)
|
||||
const [editing, setEditing] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
@ -77,7 +79,7 @@ export default function PreferencePage() {
|
||||
try {
|
||||
await updateUserProfile({ url, body: { [bodyKey]: item.value } })
|
||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||
mutateUserProfile()
|
||||
refreshUserProfile()
|
||||
}
|
||||
catch (e) {
|
||||
toast.error((e as Error).message)
|
||||
|
||||
@ -7,12 +7,13 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle, DialogTrigger } from '@langgenius/dify-ui/dialog'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { convertTimezoneToOffsetStr } from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
||||
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
||||
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds, dayjsToTimeOfDay, timeOfDayToDayjs } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/utils'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileAtom } from '@/context/app-context-state'
|
||||
import { useMutationPluginAutoUpgradeSettings, usePluginAutoUpgradeSettings } from '@/service/use-plugins'
|
||||
import UpdateSettingDialogForm from './update-setting-dialog-form'
|
||||
|
||||
@ -26,7 +27,7 @@ const UpdateSettingDialog = ({
|
||||
disabled = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile } = useAppContext()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const timezone = userProfile.timezone || 'UTC'
|
||||
const {
|
||||
data: autoUpgradeSetting,
|
||||
|
||||
@ -4,10 +4,24 @@ import { vi } from 'vitest'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import EnvNav from '../index'
|
||||
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
current: {} as Partial<AppContextValue>,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
describe('EnvNav', () => {
|
||||
const mockUseAppContext = vi.mocked(useAppContext)
|
||||
|
||||
@ -16,33 +30,39 @@ describe('EnvNav', () => {
|
||||
})
|
||||
|
||||
it('should render null when environment is PRODUCTION', () => {
|
||||
mockUseAppContext.mockReturnValue({
|
||||
const appContextValue = {
|
||||
langGeniusVersionInfo: {
|
||||
current_env: 'PRODUCTION',
|
||||
},
|
||||
} as unknown as AppContextValue)
|
||||
} as unknown as AppContextValue
|
||||
mockAppContextState.current = appContextValue
|
||||
mockUseAppContext.mockReturnValue(appContextValue)
|
||||
|
||||
const { container } = render(<EnvNav />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should render TESTING tag and icon when environment is TESTING', () => {
|
||||
mockUseAppContext.mockReturnValue({
|
||||
const appContextValue = {
|
||||
langGeniusVersionInfo: {
|
||||
current_env: 'TESTING',
|
||||
},
|
||||
} as unknown as AppContextValue)
|
||||
} as unknown as AppContextValue
|
||||
mockAppContextState.current = appContextValue
|
||||
mockUseAppContext.mockReturnValue(appContextValue)
|
||||
|
||||
render(<EnvNav />)
|
||||
expect(screen.getByText('common.environment.testing')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render DEVELOPMENT tag and icon when environment is DEVELOPMENT', () => {
|
||||
mockUseAppContext.mockReturnValue({
|
||||
const appContextValue = {
|
||||
langGeniusVersionInfo: {
|
||||
current_env: 'DEVELOPMENT',
|
||||
},
|
||||
} as unknown as AppContextValue)
|
||||
} as unknown as AppContextValue
|
||||
mockAppContextState.current = appContextValue
|
||||
mockUseAppContext.mockReturnValue(appContextValue)
|
||||
|
||||
render(<EnvNav />)
|
||||
expect(
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TerminalSquare } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { Beaker02 } from '@/app/components/base/icons/src/vender/solid/education'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { langGeniusVersionInfoAtom } from '@/context/app-context-state'
|
||||
|
||||
const headerEnvClassName: { [k: string]: string } = {
|
||||
DEVELOPMENT: 'bg-[#FEC84B] border-[#FDB022] text-[#93370D]',
|
||||
@ -12,7 +13,7 @@ const headerEnvClassName: { [k: string]: string } = {
|
||||
|
||||
const EnvNav = () => {
|
||||
const { t } = useTranslation()
|
||||
const { langGeniusVersionInfo } = useAppContext()
|
||||
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
|
||||
const showEnvTag = langGeniusVersionInfo.current_env === 'TESTING' || langGeniusVersionInfo.current_env === 'DEVELOPMENT'
|
||||
|
||||
if (!showEnvTag)
|
||||
|
||||
@ -30,6 +30,9 @@ const { mockIsAgentV2Enabled, mockSwitchWorkspace, mockToastSuccess } = vi.hoist
|
||||
mockToastSuccess: vi.fn(),
|
||||
mockIsAgentV2Enabled: vi.fn(() => true),
|
||||
}))
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
current: undefined as AppContextValue | undefined,
|
||||
}))
|
||||
|
||||
vi.mock('@/features/agent-v2/feature-flag', () => ({
|
||||
isAgentV2Enabled: () => mockIsAgentV2Enabled(),
|
||||
@ -40,6 +43,16 @@ vi.mock('@/context/app-context', () => ({
|
||||
useSelector: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current ?? {})
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: vi.fn(),
|
||||
}))
|
||||
@ -240,6 +253,7 @@ const renderMainNav = (
|
||||
const queryClient = createTestQueryClient()
|
||||
const getMockAppContext = useAppContext as Mock
|
||||
const currentAppContext = getMockAppContext() as AppContextValue
|
||||
mockAppContextState.current = currentAppContext
|
||||
queryClient.setQueryData(consoleQuery.workspaces.current.post.queryKey(), currentAppContext.currentWorkspace)
|
||||
queryClient.setQueryData(consoleQuery.workspaces.get.queryKey(), { workspaces: mockWorkspaces })
|
||||
const resolvedSystemFeatures = {
|
||||
@ -287,6 +301,7 @@ describe('MainNav', () => {
|
||||
refresh: vi.fn(),
|
||||
})
|
||||
;(useAppContext as Mock).mockReturnValue(appContextValue)
|
||||
mockAppContextState.current = appContextValue
|
||||
;(useAppContextSelector as Mock).mockImplementation((selector: (state: AppContextValue) => unknown) => selector((useAppContext as Mock)() as AppContextValue))
|
||||
;(useProviderContext as Mock).mockReturnValue({
|
||||
enableBilling: true,
|
||||
|
||||
@ -29,6 +29,18 @@ vi.mock('@/context/app-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
workspacePermissionKeys: mockWorkspacePermissionKeys.value,
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/snippets/hooks/use-create-snippet', async () => {
|
||||
const React = await vi.importActual<typeof import('react')>('react')
|
||||
|
||||
|
||||
@ -6,6 +6,13 @@ import { CommentIcon } from './comment-icon'
|
||||
type Position = { x: number, y: number }
|
||||
|
||||
let mockUserId = 'user-1'
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
name: 'User',
|
||||
avatar_url: 'avatar',
|
||||
},
|
||||
}))
|
||||
|
||||
const mockFlowToScreenPosition = vi.fn((position: Position) => position)
|
||||
const mockScreenToFlowPosition = vi.fn((position: Position) => position)
|
||||
@ -25,13 +32,28 @@ vi.mock('reactflow', () => ({
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
userProfile: {
|
||||
...mockAppContextState.userProfile,
|
||||
id: mockUserId,
|
||||
name: 'User',
|
||||
avatar_url: 'avatar',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
...mockAppContextState,
|
||||
userProfile: {
|
||||
...mockAppContextState.userProfile,
|
||||
id: mockUserId,
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/user-avatar-list', () => ({
|
||||
UserAvatarList: ({ users }: { users: Array<{ id: string }> }) => (
|
||||
<div data-testid="avatar-list">{users.map(user => user.id).join(',')}</div>
|
||||
|
||||
@ -2,10 +2,11 @@
|
||||
|
||||
import type { FC, PointerEvent as ReactPointerEvent } from 'react'
|
||||
import type { WorkflowCommentList } from '@/app/components/workflow/comment/types'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useReactFlow, useViewport } from 'reactflow'
|
||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileIdAtom } from '@/context/app-context-state'
|
||||
import CommentPreview from './comment-preview'
|
||||
|
||||
type CommentIconProps = {
|
||||
@ -18,8 +19,8 @@ type CommentIconProps = {
|
||||
export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick, isActive = false, onPositionUpdate }) => {
|
||||
const { flowToScreenPosition, screenToFlowPosition } = useReactFlow()
|
||||
const viewport = useViewport()
|
||||
const { userProfile } = useAppContext()
|
||||
const isAuthor = comment.created_by_account?.id === userProfile?.id
|
||||
const currentUserId = useAtomValue(userProfileIdAtom)
|
||||
const isAuthor = comment.created_by_account?.id === currentUserId
|
||||
const [showPreview, setShowPreview] = useState(false)
|
||||
const [dragPosition, setDragPosition] = useState<{ x: number, y: number } | null>(null)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
|
||||
@ -18,6 +18,13 @@ const stableT = (key: string, options?: { ns?: string }) => (
|
||||
)
|
||||
|
||||
let mentionInputProps: MentionInputProps | null = null
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
name: 'Alice',
|
||||
avatar_url: 'avatar',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
@ -27,14 +34,20 @@ vi.mock('react-i18next', () => ({
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
name: 'Alice',
|
||||
avatar_url: 'avatar',
|
||||
},
|
||||
userProfile: mockAppContextState.userProfile,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@langgenius/dify-ui/avatar', () => ({
|
||||
Avatar: ({ name }: { name: string }) => <div data-testid="avatar">{name}</div>,
|
||||
default: ({ name }: { name: string }) => <div data-testid="avatar">{name}</div>,
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import type { FC, PointerEvent as ReactPointerEvent } from 'react'
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileAtom } from '@/context/app-context-state'
|
||||
import { MentionInput } from './mention-input'
|
||||
|
||||
type CommentInputProps = {
|
||||
@ -30,7 +31,7 @@ export const CommentInput: FC<CommentInputProps> = memo(({
|
||||
}) => {
|
||||
const [content, setContent] = useState('')
|
||||
const { t } = useTranslation()
|
||||
const { userProfile } = useAppContext()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const dragStateRef = useRef<{
|
||||
pointerId: number | null
|
||||
startPointerX: number
|
||||
|
||||
@ -4,6 +4,13 @@ import { CommentThread } from './thread'
|
||||
|
||||
const mockSetCommentPreviewHovering = vi.hoisted(() => vi.fn())
|
||||
const mockFlowToScreenPosition = vi.hoisted(() => vi.fn(({ x, y }: { x: number, y: number }) => ({ x, y })))
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
name: 'Alice',
|
||||
avatar_url: 'alice.png',
|
||||
},
|
||||
}))
|
||||
|
||||
const storeState = vi.hoisted(() => ({
|
||||
mentionableUsersCache: {
|
||||
@ -33,14 +40,20 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
name: 'Alice',
|
||||
avatar_url: 'alice.png',
|
||||
},
|
||||
userProfile: mockAppContextState.userProfile,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useReactFlow: () => ({
|
||||
flowToScreenPosition: mockFlowToScreenPosition,
|
||||
|
||||
@ -11,13 +11,14 @@ import {
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine, RiMoreFill } from '@remixicon/react'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useReactFlow, useViewport } from 'reactflow'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import InlineDeleteConfirm from '@/app/components/base/inline-delete-confirm'
|
||||
import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileAtom, userProfileIdAtom } from '@/context/app-context-state'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { useParams } from '@/next/navigation'
|
||||
import { useStore } from '../store'
|
||||
@ -52,8 +53,7 @@ const ThreadMessage: FC<{
|
||||
className?: string
|
||||
}> = ({ authorId, authorName, avatarUrl, createdAt, content, mentionableNames, className }) => {
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const { userProfile } = useAppContext()
|
||||
const currentUserId = userProfile?.id
|
||||
const currentUserId = useAtomValue(userProfileIdAtom)
|
||||
const isCurrentUser = authorId === currentUserId
|
||||
const userColor = isCurrentUser ? undefined : getUserColor(authorId)
|
||||
|
||||
@ -175,7 +175,8 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
const appId = params.appId as string
|
||||
const { flowToScreenPosition } = useReactFlow()
|
||||
const viewport = useViewport()
|
||||
const { userProfile } = useAppContext()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const currentUserId = userProfile.id
|
||||
const { t } = useTranslation()
|
||||
const [replyContent, setReplyContent] = useState('')
|
||||
const [editingCommentContent, setEditingCommentContent] = useState('')
|
||||
@ -369,7 +370,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
}, [editingReply, onReplyEdit])
|
||||
|
||||
const replies = comment.replies || []
|
||||
const isOwnComment = comment.created_by_account?.id === userProfile?.id
|
||||
const isOwnComment = comment.created_by_account?.id === currentUserId
|
||||
const messageListRef = useRef<HTMLDivElement>(null)
|
||||
const previousReplyCountRef = useRef<number | undefined>(undefined)
|
||||
const previousCommentIdRef = useRef<string | undefined>(undefined)
|
||||
@ -604,7 +605,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
<div className="mt-2 space-y-3 pt-3">
|
||||
{replies.map((reply) => {
|
||||
const isReplyEditing = editingReply?.id === reply.id
|
||||
const isOwnReply = reply.created_by_account?.id === userProfile?.id
|
||||
const isOwnReply = reply.created_by_account?.id === currentUserId
|
||||
return (
|
||||
<div
|
||||
key={reply.id}
|
||||
|
||||
@ -13,6 +13,22 @@ const mockHandleLoadBackupDraft = vi.fn()
|
||||
const mockHandleRefreshWorkflowDraft = vi.fn()
|
||||
let mockPlanType = Plan.professional
|
||||
let mockEnableBilling = true
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: {
|
||||
id: '',
|
||||
name: '',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
|
||||
@ -25,6 +25,22 @@ const mockViewHistory = vi.fn()
|
||||
|
||||
let mockNodesReadOnly = false
|
||||
let mockTheme: 'light' | 'dark' = 'light'
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: {
|
||||
id: '',
|
||||
name: '',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useNodes: () => mockUseNodes(),
|
||||
|
||||
@ -2,6 +2,7 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiHistoryLine } from '@remixicon/react'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import {
|
||||
useCallback,
|
||||
useState,
|
||||
@ -9,7 +10,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import { userProfileAtom } from '@/context/app-context-state'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useInvalidAllLastRun, useResetWorkflowVersionHistory, useRestoreWorkflow } from '@/service/use-workflow'
|
||||
@ -39,7 +40,7 @@ const HeaderInRestoring = ({
|
||||
const [isRestorePlanUpgradeModalOpen, setIsRestorePlanUpgradeModalOpen] = useState(false)
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const userProfile = useAppContextSelector(s => s.userProfile)
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
||||
const {
|
||||
|
||||
@ -9,10 +9,11 @@ import {
|
||||
PopoverTrigger,
|
||||
} from '@langgenius/dify-ui/popover'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileIdAtom } from '@/context/app-context-state'
|
||||
import { getAvatar } from '@/service/common'
|
||||
import { useCollaboration } from '../collaboration/hooks/use-collaboration'
|
||||
import { getUserColor } from '../collaboration/utils/user-color'
|
||||
@ -54,12 +55,11 @@ const OnlineUsers = () => {
|
||||
const { t } = useTranslation()
|
||||
const appId = useStore(s => s.appId)
|
||||
const { onlineUsers, cursors, isEnabled: isCollaborationEnabled } = useCollaboration(appId as string)
|
||||
const { userProfile } = useAppContext()
|
||||
const currentUserId = useAtomValue(userProfileIdAtom)
|
||||
const reactFlow = useReactFlow()
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||
const avatarUrls = useAvatarUrls(onlineUsers || [])
|
||||
|
||||
const currentUserId = userProfile?.id
|
||||
const fallbackUsername = t('comments.fallback.user', { ns: 'workflow' })
|
||||
const currentUserSuffix = t('members.you', { ns: 'common' })
|
||||
|
||||
|
||||
@ -28,6 +28,14 @@ const commentsUpdateState = vi.hoisted(() => ({
|
||||
const globalFeatureState = vi.hoisted(() => ({
|
||||
enableCollaboration: true,
|
||||
}))
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
avatar_url: 'alice.png',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useReactFlow: () => ({
|
||||
@ -43,15 +51,20 @@ vi.mock('@/next/navigation', () => ({
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
avatar_url: 'alice.png',
|
||||
},
|
||||
userProfile: mockAppContextState.userProfile,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleClient: {
|
||||
systemFeatures: {
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import type { UserProfile, WorkflowCommentDetail, WorkflowCommentList } from '@/app/components/workflow/comment/types'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileAtom } from '@/context/app-context-state'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useParams } from '@/next/navigation'
|
||||
import { consoleClient } from '@/service/client'
|
||||
@ -67,7 +68,7 @@ export const useWorkflowComment = () => {
|
||||
() => new Map(mentionableUsers.map(user => [user.id, user])),
|
||||
[mentionableUsers],
|
||||
)
|
||||
const { userProfile } = useAppContext()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const { data: isCollaborationEnabled } = useSuspenseQuery({
|
||||
...systemFeaturesQueryOptions(),
|
||||
select: s => s.enable_collaboration_mode,
|
||||
|
||||
@ -11,6 +11,25 @@ const mockHandleNodeIterationChildSizeChange = vi.fn()
|
||||
const mockHandleNodeLoopChildSizeChange = vi.fn()
|
||||
const mockUseNodeResizeObserver = vi.fn()
|
||||
const mockUseCollaboration = vi.fn()
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
name: 'User',
|
||||
email: 'user@example.com',
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useNodesReadOnly: () => ({ nodesReadOnly: false }),
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
RiPlayLargeLine,
|
||||
} from '@remixicon/react'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
cloneElement,
|
||||
@ -69,7 +70,7 @@ import {
|
||||
hasRetryNode,
|
||||
isSupportCustomRunForm,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileAtom } from '@/context/app-context-state'
|
||||
import { useAllBuiltInTools } from '@/service/use-tools'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { FlowType } from '@/types/common'
|
||||
@ -114,7 +115,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const appId = useStore(s => s.appId)
|
||||
const { userProfile } = useAppContext()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const { isConnected, nodePanelPresence } = useCollaboration(appId as string)
|
||||
const { showMessageLogModal } = useAppStore(useShallow(state => ({
|
||||
showMessageLogModal: state.showMessageLogModal,
|
||||
|
||||
@ -4,6 +4,7 @@ import type {
|
||||
} from 'react'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import {
|
||||
cloneElement,
|
||||
memo,
|
||||
@ -28,7 +29,7 @@ import {
|
||||
NodeRunningStatus,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { hasErrorHandleNode, hasRetryNode } from '@/app/components/workflow/utils'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileAtom } from '@/context/app-context-state'
|
||||
import { selectWorkflowNode } from '../../utils/node-navigation'
|
||||
import AddVariablePopupWithPosition from './components/add-variable-popup-with-position'
|
||||
import EntryNodeContainer, { StartNodeTypeEnum } from './components/entry-node-container'
|
||||
@ -76,7 +77,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
const { handleNodeIterationChildSizeChange } = useNodeIterationInteractions()
|
||||
const { handleNodeLoopChildSizeChange } = useNodeLoopInteractions()
|
||||
const toolIcon = useToolIcon(data)
|
||||
const { userProfile } = useAppContext()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const appId = useStore(s => s.appId)
|
||||
const { nodePanelPresence } = useCollaboration(appId as string)
|
||||
const controlMode = useStore(s => s.controlMode)
|
||||
|
||||
@ -3,7 +3,11 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import EmailConfigureModal from '../email-configure-modal'
|
||||
|
||||
const mockToastError = vi.hoisted(() => vi.fn())
|
||||
const mockUseAppContextSelector = vi.hoisted(() => vi.fn())
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: {
|
||||
email: 'owner@example.com',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
toast: {
|
||||
@ -13,9 +17,19 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: (selector: (state: { userProfile: { email: string } }) => string) =>
|
||||
mockUseAppContextSelector(selector),
|
||||
selector({ userProfile: mockAppContextState.userProfile }),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('../mail-body-input', () => ({
|
||||
default: ({ value, onChange }: { value: string, onChange: (value: string) => void }) => (
|
||||
<textarea
|
||||
@ -80,11 +94,6 @@ const createEmailConfig = (overrides: Partial<EmailConfig> = {}): EmailConfig =>
|
||||
describe('human-input/delivery-method/email-configure-modal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseAppContextSelector.mockImplementation(selector => selector({
|
||||
userProfile: {
|
||||
email: 'owner@example.com',
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should save a valid email configuration with updated values', () => {
|
||||
|
||||
@ -18,15 +18,29 @@ type TestEmailSenderProps = {
|
||||
jumpToEmailConfigModal: () => void
|
||||
}
|
||||
|
||||
const mockUseAppContextSelector = vi.hoisted(() => vi.fn())
|
||||
const mockEmailConfigureModal = vi.hoisted(() => vi.fn())
|
||||
const mockTestEmailSender = vi.hoisted(() => vi.fn())
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: {
|
||||
email: 'owner@example.com',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: (selector: (state: { userProfile: { email: string } }) => string) =>
|
||||
mockUseAppContextSelector(selector),
|
||||
selector({ userProfile: mockAppContextState.userProfile }),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('../email-configure-modal', () => ({
|
||||
default: (props: EmailConfigureModalProps) => {
|
||||
mockEmailConfigureModal(props)
|
||||
@ -107,11 +121,6 @@ const getMethodRow = (label: string) => {
|
||||
describe('human-input/delivery-method/method-item', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseAppContextSelector.mockImplementation(selector => selector({
|
||||
userProfile: {
|
||||
email: 'owner@example.com',
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should toggle and delete a webapp delivery method', () => {
|
||||
|
||||
@ -21,6 +21,28 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
email: 'owner@example.com',
|
||||
name: 'Owner',
|
||||
},
|
||||
currentWorkspace: {
|
||||
id: 'workspace-1',
|
||||
name: 'Product Team',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
type RecordedRequest = {
|
||||
url: string
|
||||
method: string
|
||||
@ -48,14 +70,11 @@ const renderWithProviders = (ui: ReactNode) => {
|
||||
value={{
|
||||
userProfile: {
|
||||
...userProfilePlaceholder,
|
||||
id: 'user-1',
|
||||
email: 'owner@example.com',
|
||||
name: 'Owner',
|
||||
...mockAppContextState.userProfile,
|
||||
},
|
||||
currentWorkspace: {
|
||||
...initialWorkspaceInfo,
|
||||
id: 'workspace-1',
|
||||
name: 'Product Team',
|
||||
...mockAppContextState.currentWorkspace,
|
||||
},
|
||||
isCurrentWorkspaceManager: true,
|
||||
isCurrentWorkspaceOwner: true,
|
||||
@ -67,14 +86,11 @@ const renderWithProviders = (ui: ReactNode) => {
|
||||
useSelector: selector => selector({
|
||||
userProfile: {
|
||||
...userProfilePlaceholder,
|
||||
id: 'user-1',
|
||||
email: 'owner@example.com',
|
||||
name: 'Owner',
|
||||
...mockAppContextState.userProfile,
|
||||
},
|
||||
currentWorkspace: {
|
||||
...initialWorkspaceInfo,
|
||||
id: 'workspace-1',
|
||||
name: 'Product Team',
|
||||
...mockAppContextState.currentWorkspace,
|
||||
},
|
||||
isCurrentWorkspaceManager: true,
|
||||
isCurrentWorkspaceOwner: true,
|
||||
|
||||
@ -8,10 +8,11 @@ import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgeni
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiBugLine } from '@remixicon/react'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { userProfileEmailAtom } from '@/context/app-context-state'
|
||||
import MailBodyInput from './mail-body-input'
|
||||
import Recipient from './recipient'
|
||||
|
||||
@ -35,7 +36,7 @@ const EmailConfigureModal = ({
|
||||
availableNodes = [],
|
||||
}: EmailConfigureModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const email = useAppContextWithSelector(s => s.userProfile.email)
|
||||
const email = useAtomValue(userProfileEmailAtom)
|
||||
const [recipients, setRecipients] = useState(config?.recipients || { whole_workspace: false, items: [] })
|
||||
const [subject, setSubject] = useState(config?.subject || '')
|
||||
const [body, setBody] = useState(config?.body || '{{#url#}}')
|
||||
|
||||
@ -16,11 +16,12 @@ import {
|
||||
RiRobot2Fill,
|
||||
RiSendPlane2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import Badge from '@/app/components/base/badge/index'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { userProfileEmailAtom } from '@/context/app-context-state'
|
||||
import { DeliveryMethodType } from '../../types'
|
||||
import EmailConfigureModal from './email-configure-modal'
|
||||
import TestEmailSender from './test-email-sender'
|
||||
@ -51,7 +52,7 @@ const DeliveryMethodItem: FC<DeliveryMethodItemProps> = ({
|
||||
readonly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const email = useAppContextWithSelector(s => s.userProfile.email)
|
||||
const email = useAtomValue(userProfileEmailAtom)
|
||||
const [isHovering, setIsHovering] = useState(false)
|
||||
const [showEmailModal, setShowEmailModal] = useState(false)
|
||||
const [showTestEmailModal, setShowTestEmailModal] = useState(false)
|
||||
|
||||
@ -4,6 +4,10 @@ import Recipient from '../index'
|
||||
const mockUseTranslation = vi.hoisted(() => vi.fn())
|
||||
const mockUseAppContext = vi.hoisted(() => vi.fn())
|
||||
const mockUseMembers = vi.hoisted(() => vi.fn())
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: { email: 'owner@example.com' },
|
||||
currentWorkspace: { name: 'Dify\'s Lab' },
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => mockUseTranslation(),
|
||||
@ -13,6 +17,16 @@ vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => mockUseAppContext(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useMembers: () => mockUseMembers(),
|
||||
}))
|
||||
@ -69,10 +83,7 @@ describe('Recipient', () => {
|
||||
mockUseTranslation.mockReturnValue({
|
||||
t: (key: string, options?: { workspaceName?: string }) => options?.workspaceName ?? key,
|
||||
})
|
||||
mockUseAppContext.mockReturnValue({
|
||||
userProfile: { email: 'owner@example.com' },
|
||||
currentWorkspace: { name: 'Dify\'s Lab' },
|
||||
})
|
||||
mockUseAppContext.mockReturnValue(mockAppContextState)
|
||||
mockUseMembers.mockReturnValue({
|
||||
data: {
|
||||
accounts: [
|
||||
|
||||
@ -3,9 +3,10 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { RiGroupLine } from '@remixicon/react'
|
||||
import { produce } from 'immer'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { currentWorkspaceAtom, userProfileEmailAtom } from '@/context/app-context-state'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
import EmailInput from './email-input'
|
||||
import MemberSelector from './member-selector'
|
||||
@ -22,7 +23,8 @@ const Recipient = ({
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile, currentWorkspace } = useAppContext()
|
||||
const userProfileEmail = useAtomValue(userProfileEmailAtom)
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
const { data: members } = useMembers()
|
||||
const accounts = members?.accounts || []
|
||||
|
||||
@ -70,14 +72,14 @@ const Recipient = ({
|
||||
<div className="w-[86px]">
|
||||
<MemberSelector
|
||||
value={data.items}
|
||||
email={userProfile.email}
|
||||
email={userProfileEmail}
|
||||
list={accounts}
|
||||
onSelect={handleMemberSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<EmailInput
|
||||
email={userProfile.email}
|
||||
email={userProfileEmail}
|
||||
value={data.items}
|
||||
list={accounts}
|
||||
onDelete={handleDelete}
|
||||
|
||||
@ -10,6 +10,7 @@ import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgeni
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiArrowRightSFill } from '@remixicon/react'
|
||||
import { noop, unionBy } from 'es-toolkit/compat'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
@ -24,7 +25,7 @@ import {
|
||||
isSystemVar,
|
||||
} from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { InputVarType, VarType } from '@/app/components/workflow/types'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { currentWorkspaceAtom, userProfileEmailAtom } from '@/context/app-context-state'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
import { useTestEmailSender } from '@/service/use-workflow'
|
||||
import { getHumanInputFormDependencySelectors, isOutput } from '../../utils'
|
||||
@ -123,7 +124,8 @@ const EmailSenderModal = ({
|
||||
availableNodes = [],
|
||||
}: EmailSenderModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile, currentWorkspace } = useAppContext()
|
||||
const userProfileEmail = useAtomValue(userProfileEmailAtom)
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const { mutateAsync: testEmailSender } = useTestEmailSender()
|
||||
|
||||
@ -236,7 +238,7 @@ const EmailSenderModal = ({
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.debugDone`}
|
||||
ns="workflow"
|
||||
components={{ email: <span className="system-md-semibold text-text-secondary"></span> }}
|
||||
values={{ email: userProfile.email }}
|
||||
values={{ email: userProfileEmail }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -268,7 +270,7 @@ const EmailSenderModal = ({
|
||||
<div className="mt-4">
|
||||
<EmailInput
|
||||
disabled
|
||||
email={userProfile.email}
|
||||
email={userProfileEmail}
|
||||
value={config?.recipients?.items}
|
||||
list={accounts}
|
||||
onDelete={noop}
|
||||
@ -308,7 +310,7 @@ const EmailSenderModal = ({
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip2`}
|
||||
ns="workflow"
|
||||
components={{ email: <span className="system-sm-semibold text-text-primary"></span> }}
|
||||
values={{ email: userProfile.email }}
|
||||
values={{ email: userProfileEmail }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@ -342,7 +344,7 @@ const EmailSenderModal = ({
|
||||
<div className="mt-4">
|
||||
<EmailInput
|
||||
disabled
|
||||
email={userProfile.email}
|
||||
email={userProfileEmail}
|
||||
value={config?.recipients?.items}
|
||||
list={accounts}
|
||||
onDelete={noop}
|
||||
|
||||
@ -141,6 +141,21 @@ vi.mock('@/context/app-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: { id: 'user-1' },
|
||||
workspacePermissionKeys: [] as string[],
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/utils/permission', () => ({
|
||||
DatasetACLPermission: {
|
||||
Readonly: 'dataset.acl.readonly',
|
||||
|
||||
@ -2,10 +2,11 @@
|
||||
import type { FC } from 'react'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { produce } from 'immer'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import { userProfileIdAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||
import { getDatasetACLCapabilities } from '@/utils/permission'
|
||||
import Item from './dataset-item'
|
||||
|
||||
@ -29,8 +30,8 @@ const DatasetList: FC<Props> = ({
|
||||
settingsModalHeight,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const currentUserId = useAppContextSelector(s => s.userProfile?.id)
|
||||
const workspacePermissionKeys = useAppContextSelector(s => s.workspacePermissionKeys)
|
||||
const currentUserId = useAtomValue(userProfileIdAtom)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
|
||||
const handleRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { NoteNodeType } from '../note-node/types'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback } from 'react'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileAtom } from '@/context/app-context-state'
|
||||
import {
|
||||
CUSTOM_NOTE_NODE,
|
||||
} from '../note-node/constants'
|
||||
@ -11,7 +12,7 @@ import { generateNewNode } from '../utils'
|
||||
|
||||
export const useOperator = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { userProfile } = useAppContext()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const showAuthorStorage = useWorkflowNoteShowAuthorValue()
|
||||
|
||||
const handleAddNote = useCallback(() => {
|
||||
|
||||
@ -7,6 +7,9 @@ const mockHandleCommentResolve = vi.hoisted(() => vi.fn())
|
||||
const mockSetActiveCommentId = vi.hoisted(() => vi.fn())
|
||||
const mockSetControlMode = vi.hoisted(() => vi.fn())
|
||||
const mockSetShowResolvedComments = vi.hoisted(() => vi.fn())
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: { id: 'user-1' },
|
||||
}))
|
||||
|
||||
const commentFixtures: WorkflowCommentList[] = [
|
||||
{
|
||||
@ -70,10 +73,20 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
userProfile: { id: 'user-1' },
|
||||
userProfile: mockAppContextState.userProfile,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: (selector: (state: WorkflowStoreSelectionState) => unknown) => selector({
|
||||
activeCommentId: storeState.activeCommentId,
|
||||
|
||||
@ -2,6 +2,7 @@ import type { WorkflowCommentList } from '@/app/components/workflow/comment/type
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { RiCheckboxCircleFill, RiCheckboxCircleLine, RiCheckLine, RiCloseLine, RiFilter3Line } from '@remixicon/react'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
@ -9,7 +10,7 @@ import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||
import { useWorkflowComment } from '@/app/components/workflow/hooks/use-workflow-comment'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { ControlMode } from '@/app/components/workflow/types'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileIdAtom } from '@/context/app-context-state'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
|
||||
const CommentsPanel = () => {
|
||||
@ -29,16 +30,16 @@ const CommentsPanel = () => {
|
||||
handleCommentIconClick(comment)
|
||||
}, [handleCommentIconClick])
|
||||
|
||||
const { userProfile } = useAppContext()
|
||||
const currentUserId = useAtomValue(userProfileIdAtom)
|
||||
|
||||
const filteredSorted = useMemo(() => {
|
||||
let data = comments
|
||||
if (!showResolvedComments)
|
||||
data = data.filter(c => !c.resolved)
|
||||
if (showOnlyMine)
|
||||
data = data.filter(c => c.created_by === userProfile?.id)
|
||||
data = data.filter(c => c.created_by === currentUserId)
|
||||
return data
|
||||
}, [comments, showOnlyMine, showResolvedComments, userProfile?.id])
|
||||
}, [comments, currentUserId, showOnlyMine, showResolvedComments])
|
||||
|
||||
const handleResolve = useCallback(async (comment: WorkflowCommentList) => {
|
||||
if (comment.resolved)
|
||||
|
||||
@ -18,6 +18,12 @@ const mockEmitRestoreComplete = vi.fn()
|
||||
const mockEmitWorkflowUpdate = vi.fn()
|
||||
let mockPlanType = Plan.professional
|
||||
let mockEnableBilling = true
|
||||
const mockAppContextState = vi.hoisted(() => ({
|
||||
userProfile: {
|
||||
id: 'test-user-id',
|
||||
name: 'Test User',
|
||||
},
|
||||
}))
|
||||
|
||||
const createVersionHistory = (overrides: Partial<VersionHistory> = {}): VersionHistory => ({
|
||||
id: 'version-id',
|
||||
@ -60,9 +66,19 @@ type MockVersionHistoryItemProps = {
|
||||
}
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: () => ({ id: 'test-user-id' }),
|
||||
useSelector: () => mockAppContextState.userProfile,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
plan: { type: mockPlanType },
|
||||
|
||||
@ -3,6 +3,7 @@ import type { VersionHistory } from '@/types/workflow'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiArrowDownDoubleLine, RiCloseLine, RiLoader2Line } from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -10,7 +11,7 @@ import VersionInfoModal from '@/app/components/app/app-publisher/version-info-mo
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import { userProfileAtom } from '@/context/app-context-state'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useDeleteWorkflow, useInvalidAllLastRun, useResetWorkflowVersionHistory, useRestoreWorkflow, useUpdateWorkflow, useWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
import { useDSL, useWorkflowRefreshDraft, useWorkflowRun } from '../../hooks'
|
||||
@ -58,7 +59,7 @@ export const VersionHistoryPanel = ({
|
||||
const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel)
|
||||
const currentVersion = useStore(s => s.currentVersion)
|
||||
const setCurrentVersion = useStore(s => s.setCurrentVersion)
|
||||
const userProfile = useAppContextSelector(s => s.userProfile)
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const canImportExportDSL = useHooksStore(s => s.accessControl.canImportExportDSL)
|
||||
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
ContextMenuSeparator,
|
||||
} from '@langgenius/dify-ui/context-menu'
|
||||
import { produce } from 'immer'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
@ -15,7 +16,7 @@ import { useStore as useReactFlowStore } from 'reactflow'
|
||||
import { useCreateSnippetFromSelection } from '@/app/components/snippets/hooks/use-create-snippet-from-selection'
|
||||
import { canCreateAndModifySnippets } from '@/app/components/snippets/utils/permission'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||
import { useNodesInteractions, useNodesReadOnly, useNodesSyncDraft } from './hooks'
|
||||
import { useWorkflowHistory, WorkflowHistoryEvent } from './hooks/use-workflow-history'
|
||||
import { ShortcutKbd } from './shortcuts/shortcut-kbd'
|
||||
@ -235,7 +236,7 @@ export function SelectionContextmenu({
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const { handleNodesCopy, handleNodesDelete, handleNodesDuplicate } = useNodesInteractions()
|
||||
const isSelectionContextMenu = useStore(s => s.contextMenuTarget?.type === 'selection')
|
||||
|
||||
|
||||
@ -142,11 +142,19 @@ vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => 'https://docs.example.com',
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: (selector: (value: { currentWorkspace: { id: string } }) => string) => selector({
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
currentWorkspace: { id: 'workspace-123' },
|
||||
}),
|
||||
}))
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useAllBuiltInTools: () => ({ data: mockBuiltInTools }),
|
||||
|
||||
@ -2,9 +2,10 @@
|
||||
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { AgentTool } from '@/features/agent-v2/agent-composer/form-state'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useMemo } from 'react'
|
||||
import { API_PREFIX } from '@/config'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
import { currentWorkspaceIdAtom } from '@/context/app-context-state'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
@ -67,7 +68,7 @@ function createProviderMap(providers: ToolWithProvider[]) {
|
||||
|
||||
export function useAgentPromptToolIconResolver() {
|
||||
const { theme } = useTheme()
|
||||
const currentWorkspaceId = useSelector(s => s.currentWorkspace.id)
|
||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom)
|
||||
const { data: builtInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
|
||||
@ -96,14 +96,22 @@ vi.mock('@/app/components/base/chat/chat/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
userProfile: {
|
||||
avatar_url: '',
|
||||
name: 'User',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useTextGenerationCurrentProviderAndModelAndModelList: () => ({
|
||||
|
||||
@ -58,16 +58,23 @@ vi.mock('@/service/client', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: <T,>(selector: (state: { userProfile: { id: string, name: string, email: string } }) => T) =>
|
||||
selector({
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
|
||||
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('jotai', async (importOriginal) => {
|
||||
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||
|
||||
return createAppContextStateJotaiMock(importOriginal)
|
||||
})
|
||||
|
||||
describe('AgentPreviewVersionsPanel', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@ -35,7 +35,7 @@ import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/comp
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
import { InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { userProfileAtom } from '@/context/app-context-state'
|
||||
import { agentComposerModelAtom } from '@/features/agent-v2/agent-composer/store-modules/model'
|
||||
import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt'
|
||||
import { ENABLE_AGENT_CLI_TOOLS } from '@/features/agent-v2/agent-detail/configure/feature-flags'
|
||||
@ -589,7 +589,7 @@ function AgentPreviewChatSession({
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const queryClient = useQueryClient()
|
||||
const { userProfile } = useAppContext()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const prompt = useAtomValue(agentComposerPromptAtom)
|
||||
const currentModel = useAtomValue(agentComposerModelAtom)
|
||||
const config = useMemo(() => buildChatConfig({
|
||||
|
||||
@ -2,9 +2,10 @@
|
||||
|
||||
import type { AgentVersionFilter } from './filter'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import { userProfileAtom } from '@/context/app-context-state'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { CurrentDraftItem } from './current-draft-item'
|
||||
import { VersionFilter } from './filter'
|
||||
@ -27,7 +28,7 @@ export function AgentPreviewVersionsPanel({
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const { t: tWorkflow } = useTranslation('workflow')
|
||||
const userProfile = useAppContextSelector(state => state.userProfile)
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const [filterValue, setFilterValue] = useState<AgentVersionFilter>('all')
|
||||
const versionsQuery = useQuery(consoleQuery.agent.byAgentId.versions.get.queryOptions({
|
||||
input: {
|
||||
|
||||
Reference in New Issue
Block a user