Compare commits

..

6 Commits

Author SHA1 Message Date
yyh
c1bea059b7 Merge branch 'main' into refactor/remove-workspace-mutate-from-context 2026-02-11 16:54:30 +08:00
yyh
be97957237 fix 2026-02-11 16:22:58 +08:00
yyh
64950df9cf update 2026-02-11 16:11:11 +08:00
yyh
dfa46cd113 Merge remote-tracking branch 'origin/main' into refactor/remove-workspace-mutate-from-context 2026-02-11 16:09:00 +08:00
Stephen Zhou
64c8d6004d Merge branch 'main' into refactor/remove-workspace-mutate-from-context 2026-02-11 15:56:48 +08:00
yyh
72b96ba972 refactor(web): remove mutateCurrentWorkspace from AppContext, use service-layer invalidation hook
The previous implementation stored query invalidation logic (mutateCurrentWorkspace) and
isFetching state (isValidatingCurrentWorkspace) in React Context, causing QuotaPanel to
flash a loading spinner on every settings tab switch due to a useEffect calling
invalidateQueries on mount. This violated separation of concerns and React best practices.

- Remove mutateCurrentWorkspace and isValidatingCurrentWorkspace from AppContext
- Add useInvalidateCurrentWorkspace hook in service layer (consistent with project pattern)
- Remove redundant useEffect + invalidateQueries in ModelProviderPage
- QuotaPanel now derives loading from !currentWorkspace.id instead of external prop
- Update custom-web-app-brand to use the new service-layer hook
2026-02-09 18:32:15 +08:00
30 changed files with 212 additions and 248 deletions

View File

@@ -89,7 +89,7 @@ jobs:
uses: actions/setup-node@v6
if: steps.changed-files.outputs.any_changed == 'true'
with:
node-version: 22
node-version: 24
cache: pnpm
cache-dependency-path: ./web/pnpm-lock.yaml

View File

@@ -28,7 +28,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: ''
cache-dependency-path: 'pnpm-lock.yaml'

View File

@@ -57,7 +57,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: pnpm
cache-dependency-path: ./web/pnpm-lock.yaml

View File

@@ -31,7 +31,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: pnpm
cache-dependency-path: ./web/pnpm-lock.yaml
@@ -398,7 +398,7 @@ jobs:
uses: actions/setup-node@v6
if: steps.changed-files.outputs.any_changed == 'true'
with:
node-version: 22
node-version: 24
cache: pnpm
cache-dependency-path: ./web/pnpm-lock.yaml

View File

@@ -157,7 +157,7 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline):
id=self._message_id,
mode=self._conversation_mode,
message_id=self._message_id,
answer=self._task_state.llm_result.message.get_text_content(),
answer=cast(str, self._task_state.llm_result.message.content),
created_at=self._message_created_at,
**extras,
),
@@ -170,7 +170,7 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline):
mode=self._conversation_mode,
conversation_id=self._conversation_id,
message_id=self._message_id,
answer=self._task_state.llm_result.message.get_text_content(),
answer=cast(str, self._task_state.llm_result.message.content),
created_at=self._message_created_at,
**extras,
),
@@ -283,7 +283,7 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline):
# handle output moderation
output_moderation_answer = self.handle_output_moderation_when_task_finished(
self._task_state.llm_result.message.get_text_content()
cast(str, self._task_state.llm_result.message.content)
)
if output_moderation_answer:
self._task_state.llm_result.message.content = output_moderation_answer
@@ -397,7 +397,7 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline):
message.message_unit_price = usage.prompt_unit_price
message.message_price_unit = usage.prompt_price_unit
message.answer = (
PromptTemplateParser.remove_template_variables(llm_result.message.get_text_content().strip())
PromptTemplateParser.remove_template_variables(cast(str, llm_result.message.content).strip())
if llm_result.message.content
else ""
)

View File

@@ -1,6 +1,6 @@
[project]
name = "dify-api"
version = "1.13.0"
version = "1.12.1"
requires-python = ">=3.11,<3.13"
dependencies = [

View File

@@ -131,54 +131,33 @@ class AppGenerateService:
elif app_model.mode == AppMode.ADVANCED_CHAT:
workflow_id = args.get("workflow_id")
workflow = cls._get_workflow(app_model, invoke_from, workflow_id)
if streaming:
# Streaming mode: subscribe to SSE and enqueue the execution on first subscriber
with rate_limit_context(rate_limit, request_id):
payload = AppExecutionParams.new(
app_model=app_model,
workflow=workflow,
user=user,
args=args,
invoke_from=invoke_from,
streaming=True,
call_depth=0,
)
payload_json = payload.model_dump_json()
def on_subscribe():
workflow_based_app_execution_task.delay(payload_json)
on_subscribe = cls._build_streaming_task_on_subscribe(on_subscribe)
generator = AdvancedChatAppGenerator()
return rate_limit.generate(
generator.convert_to_event_stream(
generator.retrieve_events(
AppMode.ADVANCED_CHAT,
payload.workflow_run_id,
on_subscribe=on_subscribe,
),
),
request_id=request_id,
with rate_limit_context(rate_limit, request_id):
payload = AppExecutionParams.new(
app_model=app_model,
workflow=workflow,
user=user,
args=args,
invoke_from=invoke_from,
streaming=streaming,
call_depth=0,
)
else:
# Blocking mode: run synchronously and return JSON instead of SSE
# Keep behaviour consistent with WORKFLOW blocking branch.
advanced_generator = AdvancedChatAppGenerator()
return rate_limit.generate(
advanced_generator.convert_to_event_stream(
advanced_generator.generate(
app_model=app_model,
workflow=workflow,
user=user,
args=args,
invoke_from=invoke_from,
workflow_run_id=str(uuid.uuid4()),
streaming=False,
)
payload_json = payload.model_dump_json()
def on_subscribe():
workflow_based_app_execution_task.delay(payload_json)
on_subscribe = cls._build_streaming_task_on_subscribe(on_subscribe)
generator = AdvancedChatAppGenerator()
return rate_limit.generate(
generator.convert_to_event_stream(
generator.retrieve_events(
AppMode.ADVANCED_CHAT,
payload.workflow_run_id,
on_subscribe=on_subscribe,
),
request_id=request_id,
)
),
request_id=request_id,
)
elif app_model.mode == AppMode.WORKFLOW:
workflow_id = args.get("workflow_id")
workflow = cls._get_workflow(app_model, invoke_from, workflow_id)

View File

@@ -63,56 +63,3 @@ def test_workflow_blocking_injects_pause_state_config(mocker, monkeypatch):
pause_state_config = call_kwargs.get("pause_state_config")
assert pause_state_config is not None
assert pause_state_config.state_owner_user_id == "owner-id"
def test_advanced_chat_blocking_returns_dict_and_does_not_use_event_retrieval(mocker, monkeypatch):
"""
Regression test: ADVANCED_CHAT in blocking mode should return a plain dict
(non-streaming), and must not go through the async retrieve_events path.
Keeps behavior consistent with WORKFLOW blocking branch.
"""
# Disable billing and stub RateLimit to a no-op that just passes values through
monkeypatch.setattr(app_generate_service_module.dify_config, "BILLING_ENABLED", False)
mocker.patch("services.app_generate_service.RateLimit", _DummyRateLimit)
# Arrange a fake workflow and wire AppGenerateService._get_workflow to return it
workflow = MagicMock()
workflow.id = "workflow-id"
mocker.patch.object(AppGenerateService, "_get_workflow", return_value=workflow)
# Spy on the streaming retrieval path to ensure it's NOT called
retrieve_spy = mocker.patch("services.app_generate_service.AdvancedChatAppGenerator.retrieve_events")
# Make AdvancedChatAppGenerator.generate return a plain dict when streaming=False
generate_spy = mocker.patch(
"services.app_generate_service.AdvancedChatAppGenerator.generate",
return_value={"result": "ok"},
)
# Minimal app model for ADVANCED_CHAT
app_model = MagicMock()
app_model.mode = AppMode.ADVANCED_CHAT
app_model.id = "app-id"
app_model.tenant_id = "tenant-id"
app_model.max_active_requests = 0
app_model.is_agent = False
user = MagicMock()
user.id = "user-id"
# Must include query and inputs for AdvancedChatAppGenerator
args = {"workflow_id": "wf-1", "query": "hello", "inputs": {}}
# Act: call service with streaming=False (blocking mode)
result = AppGenerateService.generate(
app_model=app_model,
user=user,
args=args,
invoke_from=MagicMock(),
streaming=False,
)
# Assert: returns the dict from generate(), and did not call retrieve_events()
assert result == {"result": "ok"}
assert generate_spy.call_args.kwargs.get("streaming") is False
retrieve_spy.assert_not_called()

2
api/uv.lock generated
View File

@@ -1366,7 +1366,7 @@ wheels = [
[[package]]
name = "dify-api"
version = "1.13.0"
version = "1.12.1"
source = { virtual = "." }
dependencies = [
{ name = "aliyun-log-python-sdk" },

View File

@@ -21,7 +21,7 @@ services:
# API service
api:
image: langgenius/dify-api:1.13.0
image: langgenius/dify-api:1.12.1
restart: always
environment:
# Use the shared environment variables.
@@ -63,7 +63,7 @@ services:
# worker service
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
worker:
image: langgenius/dify-api:1.13.0
image: langgenius/dify-api:1.12.1
restart: always
environment:
# Use the shared environment variables.
@@ -102,7 +102,7 @@ services:
# worker_beat service
# Celery beat for scheduling periodic tasks.
worker_beat:
image: langgenius/dify-api:1.13.0
image: langgenius/dify-api:1.12.1
restart: always
environment:
# Use the shared environment variables.
@@ -132,7 +132,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.13.0
image: langgenius/dify-web:1.12.1
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}

View File

@@ -715,7 +715,7 @@ services:
# API service
api:
image: langgenius/dify-api:1.13.0
image: langgenius/dify-api:1.12.1
restart: always
environment:
# Use the shared environment variables.
@@ -757,7 +757,7 @@ services:
# worker service
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
worker:
image: langgenius/dify-api:1.13.0
image: langgenius/dify-api:1.12.1
restart: always
environment:
# Use the shared environment variables.
@@ -796,7 +796,7 @@ services:
# worker_beat service
# Celery beat for scheduling periodic tasks.
worker_beat:
image: langgenius/dify-api:1.13.0
image: langgenius/dify-api:1.12.1
restart: always
environment:
# Use the shared environment variables.
@@ -826,7 +826,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.13.0
image: langgenius/dify-web:1.12.1
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}

View File

@@ -1 +1 @@
22
24

View File

@@ -1,5 +1,5 @@
# base image
FROM node:22-alpine AS base
FROM node:24-alpine AS base
LABEL maintainer="takatost@gmail.com"
# if you located in China, you can use aliyun mirror to speed up

View File

@@ -0,0 +1,3 @@
export const OAUTH_AUTHORIZE_PENDING_KEY = 'oauth_authorize_pending'
export const REDIRECT_URL_KEY = 'oauth_redirect_url'
export const OAUTH_AUTHORIZE_PENDING_TTL = 60 * 3

View File

@@ -7,6 +7,7 @@ import {
RiMailLine,
RiTranslate2,
} from '@remixicon/react'
import dayjs from 'dayjs'
import { useRouter, useSearchParams } from 'next/navigation'
import * as React from 'react'
import { useEffect, useRef } from 'react'
@@ -16,10 +17,22 @@ import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { setPostLoginRedirect } from '@/app/signin/utils/post-login-redirect'
import { useAppContext } from '@/context/app-context'
import { useIsLogin } from '@/service/use-common'
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
import {
OAUTH_AUTHORIZE_PENDING_KEY,
OAUTH_AUTHORIZE_PENDING_TTL,
REDIRECT_URL_KEY,
} from './constants'
function setItemWithExpiry(key: string, value: string, ttl: number) {
const item = {
value,
expiry: dayjs().add(ttl, 'seconds').unix(),
}
localStorage.setItem(key, JSON.stringify(item))
}
function buildReturnUrl(pathname: string, search: string) {
try {
@@ -73,8 +86,8 @@ export default function OAuthAuthorize() {
const onLoginSwitchClick = () => {
try {
const returnUrl = buildReturnUrl('/account/oauth/authorize', `?client_id=${encodeURIComponent(client_id)}&redirect_uri=${encodeURIComponent(redirect_uri)}`)
setPostLoginRedirect(returnUrl)
router.push('/signin')
setItemWithExpiry(OAUTH_AUTHORIZE_PENDING_KEY, returnUrl, OAUTH_AUTHORIZE_PENDING_TTL)
router.push(`/signin?${REDIRECT_URL_KEY}=${encodeURIComponent(returnUrl)}`)
}
catch {
router.push('/signin')
@@ -132,7 +145,7 @@ export default function OAuthAuthorize() {
<div className="text-[var(--color-saas-dify-blue-inverted)]">{authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })}</div>
{!isLoggedIn && <div className="text-text-primary">{t('tips.notLoggedIn', { ns: 'oauth' })}</div>}
</div>
<div className="text-text-secondary body-md-regular">{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })} ${t('tips.loggedIn', { ns: 'oauth' })}` : t('tips.needLogin', { ns: 'oauth' })}</div>
<div className="body-md-regular text-text-secondary">{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })} ${t('tips.loggedIn', { ns: 'oauth' })}` : t('tips.needLogin', { ns: 'oauth' })}</div>
</div>
{isLoggedIn && userProfile && (
@@ -141,7 +154,7 @@ export default function OAuthAuthorize() {
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
<div>
<div className="system-md-semi-bold text-text-secondary">{userProfile.name}</div>
<div className="text-text-tertiary system-xs-regular">{userProfile.email}</div>
<div className="system-xs-regular text-text-tertiary">{userProfile.email}</div>
</div>
</div>
<Button variant="tertiary" size="small" onClick={onLoginSwitchClick}>{t('switchAccount', { ns: 'oauth' })}</Button>
@@ -153,7 +166,7 @@ export default function OAuthAuthorize() {
{authAppInfo!.scope.split(/\s+/).filter(Boolean).map((scope: string) => {
const Icon = SCOPE_INFO_MAP[scope]
return (
<div key={scope} className="flex items-center gap-2 text-text-secondary body-sm-medium">
<div key={scope} className="body-sm-medium flex items-center gap-2 text-text-secondary">
{Icon ? <Icon.icon className="h-4 w-4" /> : <RiAccountCircleLine className="h-4 w-4" />}
{Icon.label}
</div>
@@ -186,7 +199,7 @@ export default function OAuthAuthorize() {
</defs>
</svg>
</div>
<div className="mt-3 text-text-tertiary system-xs-regular">{t('tips.common', { ns: 'oauth' })}</div>
<div className="system-xs-regular mt-3 text-text-tertiary">{t('tips.common', { ns: 'oauth' })}</div>
</div>
)
}

View File

@@ -84,7 +84,7 @@ export const AppInitializer = ({
return
}
const redirectUrl = resolvePostLoginRedirect()
const redirectUrl = resolvePostLoginRedirect(searchParams)
if (redirectUrl) {
location.replace(redirectUrl)
return

View File

@@ -96,10 +96,8 @@ const buildAppContext = (overrides: Partial<AppContextValue> = {}): AppContextVa
isCurrentWorkspaceEditor: false,
isCurrentWorkspaceDatasetOperator: false,
mutateUserProfile: vi.fn(),
mutateCurrentWorkspace: vi.fn(),
langGeniusVersionInfo,
isLoadingCurrentWorkspace: false,
isValidatingCurrentWorkspace: false,
}
const useSelector: AppContextValue['useSelector'] = selector => selector({ ...base, useSelector })
return {

View File

@@ -7,6 +7,7 @@ import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useProviderContext } from '@/context/provider-context'
import { updateCurrentWorkspace } from '@/service/common'
import { useInvalidateCurrentWorkspace } from '@/service/use-common'
import CustomWebAppBrand from './index'
vi.mock('@/app/components/base/toast', () => ({
@@ -15,6 +16,9 @@ vi.mock('@/app/components/base/toast', () => ({
vi.mock('@/service/common', () => ({
updateCurrentWorkspace: vi.fn(),
}))
vi.mock('@/service/use-common', () => ({
useInvalidateCurrentWorkspace: vi.fn(),
}))
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
}))
@@ -37,6 +41,8 @@ const mockUseProviderContext = vi.mocked(useProviderContext)
const mockUseGlobalPublicStore = vi.mocked(useGlobalPublicStore)
const mockImageUpload = vi.mocked(imageUpload)
const mockGetImageUploadErrorMessage = vi.mocked(getImageUploadErrorMessage)
const mockInvalidateCurrentWorkspace = vi.fn()
vi.mocked(useInvalidateCurrentWorkspace).mockReturnValue(mockInvalidateCurrentWorkspace)
const defaultPlanUsage = {
buildApps: 0,
@@ -62,7 +68,6 @@ describe('CustomWebAppBrand', () => {
remove_webapp_brand: false,
},
},
mutateCurrentWorkspace: vi.fn(),
isCurrentWorkspaceManager: true,
} as any)
mockUseProviderContext.mockReturnValue({
@@ -92,7 +97,6 @@ describe('CustomWebAppBrand', () => {
remove_webapp_brand: false,
},
},
mutateCurrentWorkspace: vi.fn(),
isCurrentWorkspaceManager: false,
} as any)
@@ -101,8 +105,7 @@ describe('CustomWebAppBrand', () => {
expect(fileInput).toBeDisabled()
})
it('toggles remove brand switch and calls the backend + mutate', async () => {
const mutateMock = vi.fn()
it('toggles remove brand switch and calls the backend + invalidate', async () => {
mockUseAppContext.mockReturnValue({
currentWorkspace: {
custom_config: {
@@ -110,7 +113,6 @@ describe('CustomWebAppBrand', () => {
remove_webapp_brand: false,
},
},
mutateCurrentWorkspace: mutateMock,
isCurrentWorkspaceManager: true,
} as any)
@@ -122,7 +124,7 @@ describe('CustomWebAppBrand', () => {
url: '/workspaces/custom-config',
body: { remove_webapp_brand: true },
}))
await waitFor(() => expect(mutateMock).toHaveBeenCalled())
await waitFor(() => expect(mockInvalidateCurrentWorkspace).toHaveBeenCalled())
})
it('shows cancel/apply buttons after successful upload and cancels properly', async () => {

View File

@@ -24,6 +24,7 @@ import { useProviderContext } from '@/context/provider-context'
import {
updateCurrentWorkspace,
} from '@/service/common'
import { useInvalidateCurrentWorkspace } from '@/service/use-common'
import { cn } from '@/utils/classnames'
const ALLOW_FILE_EXTENSIONS = ['svg', 'png']
@@ -34,9 +35,9 @@ const CustomWebAppBrand = () => {
const { plan, enableBilling } = useProviderContext()
const {
currentWorkspace,
mutateCurrentWorkspace,
isCurrentWorkspaceManager,
} = useAppContext()
const invalidateCurrentWorkspace = useInvalidateCurrentWorkspace()
const [fileId, setFileId] = useState('')
const [imgKey, setImgKey] = useState(() => Date.now())
const [uploadProgress, setUploadProgress] = useState(0)
@@ -83,7 +84,7 @@ const CustomWebAppBrand = () => {
replace_webapp_logo: fileId,
},
})
mutateCurrentWorkspace()
invalidateCurrentWorkspace()
setFileId('')
setImgKey(Date.now())
}
@@ -96,7 +97,7 @@ const CustomWebAppBrand = () => {
replace_webapp_logo: '',
},
})
mutateCurrentWorkspace()
invalidateCurrentWorkspace()
}
const handleSwitch = async (checked: boolean) => {
@@ -106,7 +107,7 @@ const CustomWebAppBrand = () => {
remove_webapp_brand: checked,
},
})
mutateCurrentWorkspace()
invalidateCurrentWorkspace()
}
const handleCancel = () => {
@@ -116,7 +117,7 @@ const CustomWebAppBrand = () => {
return (
<div className="py-4">
<div className="system-md-medium mb-2 flex items-center justify-between rounded-xl bg-background-section-burn p-4 text-text-primary">
<div className="mb-2 flex items-center justify-between rounded-xl bg-background-section-burn p-4 text-text-primary system-md-medium">
{t('webapp.removeBrand', { ns: 'custom' })}
<Switch
size="l"
@@ -127,8 +128,8 @@ const CustomWebAppBrand = () => {
</div>
<div className={cn('flex h-14 items-center justify-between rounded-xl bg-background-section-burn px-4', webappBrandRemoved && 'opacity-30')}>
<div>
<div className="system-md-medium text-text-primary">{t('webapp.changeLogo', { ns: 'custom' })}</div>
<div className="system-xs-regular text-text-tertiary">{t('webapp.changeLogoTip', { ns: 'custom' })}</div>
<div className="text-text-primary system-md-medium">{t('webapp.changeLogo', { ns: 'custom' })}</div>
<div className="text-text-tertiary system-xs-regular">{t('webapp.changeLogoTip', { ns: 'custom' })}</div>
</div>
<div className="flex items-center">
{(!uploadDisabled && webappLogo && !webappBrandRemoved) && (
@@ -204,7 +205,7 @@ const CustomWebAppBrand = () => {
<div className="mt-2 text-xs text-[#D92D20]">{t('uploadedFail', { ns: 'custom' })}</div>
)}
<div className="mb-2 mt-5 flex items-center gap-2">
<div className="system-xs-medium-uppercase shrink-0 text-text-tertiary">{t('overview.appInfo.preview', { ns: 'appOverview' })}</div>
<div className="shrink-0 text-text-tertiary system-xs-medium-uppercase">{t('overview.appInfo.preview', { ns: 'appOverview' })}</div>
<Divider bgStyle="gradient" className="grow" />
</div>
<div className="relative mb-2 flex items-center gap-3">
@@ -215,7 +216,7 @@ const CustomWebAppBrand = () => {
<div className={cn('inline-flex h-8 w-8 items-center justify-center rounded-lg border border-divider-regular', 'bg-components-icon-bg-blue-light-solid')}>
<BubbleTextMod className="h-4 w-4 text-components-avatar-shape-fill-stop-100" />
</div>
<div className="system-md-semibold grow text-text-secondary">Chatflow App</div>
<div className="grow text-text-secondary system-md-semibold">Chatflow App</div>
<div className="p-1.5">
<RiLayoutLeft2Line className="h-4 w-4 text-text-tertiary" />
</div>
@@ -246,7 +247,7 @@ const CustomWebAppBrand = () => {
<div className="flex items-center gap-1.5">
{!webappBrandRemoved && (
<>
<div className="system-2xs-medium-uppercase text-text-tertiary">POWERED BY</div>
<div className="text-text-tertiary system-2xs-medium-uppercase">POWERED BY</div>
{
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img src={systemFeatures.branding.workspace_logo} alt="logo" className="block h-5 w-auto" />
@@ -262,12 +263,12 @@ const CustomWebAppBrand = () => {
<div className="flex w-[138px] grow flex-col justify-between p-2 pr-0">
<div className="flex grow flex-col justify-between rounded-l-2xl border-[0.5px] border-r-0 border-components-panel-border-subtle bg-chatbot-bg pb-4 pl-[22px] pt-16">
<div className="w-[720px] rounded-2xl border border-divider-subtle bg-chat-bubble-bg px-4 py-3">
<div className="body-md-regular mb-1 text-text-primary">Hello! How can I assist you today?</div>
<div className="mb-1 text-text-primary body-md-regular">Hello! How can I assist you today?</div>
<Button size="small">
<div className="h-2 w-[144px] rounded-sm bg-text-quaternary opacity-20"></div>
</Button>
</div>
<div className="body-lg-regular flex h-[52px] w-[578px] items-center rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pl-3.5 text-text-placeholder shadow-md backdrop-blur-sm">Talk to Dify</div>
<div className="flex h-[52px] w-[578px] items-center rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pl-3.5 text-text-placeholder shadow-md backdrop-blur-sm body-lg-regular">Talk to Dify</div>
</div>
</div>
</div>
@@ -278,14 +279,14 @@ const CustomWebAppBrand = () => {
<div className={cn('inline-flex h-8 w-8 items-center justify-center rounded-lg border border-divider-regular', 'bg-components-icon-bg-indigo-solid')}>
<RiExchange2Fill className="h-4 w-4 text-components-avatar-shape-fill-stop-100" />
</div>
<div className="system-md-semibold grow text-text-secondary">Workflow App</div>
<div className="grow text-text-secondary system-md-semibold">Workflow App</div>
<div className="p-1.5">
<RiLayoutLeft2Line className="h-4 w-4 text-text-tertiary" />
</div>
</div>
<div className="flex items-center gap-4">
<div className="system-md-semibold-uppercase flex h-10 shrink-0 items-center border-b-2 border-components-tab-active text-text-primary">RUN ONCE</div>
<div className="system-md-semibold-uppercase flex h-10 grow items-center border-b-2 border-transparent text-text-tertiary">RUN BATCH</div>
<div className="flex h-10 shrink-0 items-center border-b-2 border-components-tab-active text-text-primary system-md-semibold-uppercase">RUN ONCE</div>
<div className="flex h-10 grow items-center border-b-2 border-transparent text-text-tertiary system-md-semibold-uppercase">RUN BATCH</div>
</div>
</div>
<div className="grow bg-components-panel-bg">
@@ -293,7 +294,7 @@ const CustomWebAppBrand = () => {
<div className="mb-1 py-2">
<div className="h-2 w-20 rounded-sm bg-text-quaternary opacity-20"></div>
</div>
<div className="h-16 w-full rounded-lg bg-components-input-bg-normal "></div>
<div className="h-16 w-full rounded-lg bg-components-input-bg-normal"></div>
</div>
<div className="flex items-center justify-between px-4 py-3">
<Button size="small">
@@ -308,7 +309,7 @@ const CustomWebAppBrand = () => {
<div className="flex h-12 shrink-0 items-center gap-1.5 bg-components-panel-bg p-4 pt-3">
{!webappBrandRemoved && (
<>
<div className="system-2xs-medium-uppercase text-text-tertiary">POWERED BY</div>
<div className="text-text-tertiary system-2xs-medium-uppercase">POWERED BY</div>
{
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img src={systemFeatures.branding.workspace_logo} alt="logo" className="block h-5 w-auto" />

View File

@@ -6,10 +6,9 @@ import {
RiBrainLine,
} from '@remixicon/react'
import { useDebounce } from 'ahooks'
import { useEffect, useMemo } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useProviderContext } from '@/context/provider-context'
import { cn } from '@/utils/classnames'
@@ -34,7 +33,6 @@ const FixedModelProvider = ['langgenius/openai/openai', 'langgenius/anthropic/an
const ModelProviderPage = ({ searchText }: Props) => {
const debouncedSearchText = useDebounce(searchText, { wait: 500 })
const { t } = useTranslation()
const { mutateCurrentWorkspace, isValidatingCurrentWorkspace } = useAppContext()
const { data: textGenerationDefaultModel, isLoading: isTextGenerationDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textGeneration)
const { data: embeddingsDefaultModel, isLoading: isEmbeddingsDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textEmbedding)
const { data: rerankDefaultModel, isLoading: isRerankDefaultModelLoading } = useDefaultModel(ModelTypeEnum.rerank)
@@ -92,14 +90,10 @@ const ModelProviderPage = ({ searchText }: Props) => {
return [filteredConfiguredProviders, filteredNotConfiguredProviders]
}, [configuredProviders, debouncedSearchText, notConfiguredProviders])
useEffect(() => {
mutateCurrentWorkspace()
}, [mutateCurrentWorkspace])
return (
<div className="relative -mt-2 pt-1">
<div className={cn('mb-2 flex items-center')}>
<div className="system-md-semibold grow text-text-primary">{t('modelProvider.models', { ns: 'common' })}</div>
<div className="grow text-text-primary system-md-semibold">{t('modelProvider.models', { ns: 'common' })}</div>
<div className={cn(
'relative flex shrink-0 items-center justify-end gap-2 rounded-lg border border-transparent p-px',
defaultModelNotConfigured && 'border-components-panel-border bg-components-panel-bg-blur pl-2 shadow-xs',
@@ -107,7 +101,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
>
{defaultModelNotConfigured && <div className="absolute bottom-0 left-0 right-0 top-0 opacity-40" style={{ background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)' }} />}
{defaultModelNotConfigured && (
<div className="system-xs-medium flex items-center gap-1 text-text-primary">
<div className="flex items-center gap-1 text-text-primary system-xs-medium">
<RiAlertFill className="h-4 w-4 text-text-warning-secondary" />
<span className="max-w-[460px] truncate" title={t('modelProvider.notConfigured', { ns: 'common' })}>{t('modelProvider.notConfigured', { ns: 'common' })}</span>
</div>
@@ -123,14 +117,14 @@ const ModelProviderPage = ({ searchText }: Props) => {
/>
</div>
</div>
{IS_CLOUD_EDITION && <QuotaPanel providers={providers} isLoading={isValidatingCurrentWorkspace} />}
{IS_CLOUD_EDITION && <QuotaPanel providers={providers} />}
{!filteredConfiguredProviders?.length && (
<div className="mb-2 rounded-[10px] bg-workflow-process-bg p-4">
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg backdrop-blur">
<RiBrainLine className="h-5 w-5 text-text-primary" />
</div>
<div className="system-sm-medium mt-2 text-text-secondary">{t('modelProvider.emptyProviderTitle', { ns: 'common' })}</div>
<div className="system-xs-regular mt-1 text-text-tertiary">{t('modelProvider.emptyProviderTip', { ns: 'common' })}</div>
<div className="mt-2 text-text-secondary system-sm-medium">{t('modelProvider.emptyProviderTitle', { ns: 'common' })}</div>
<div className="mt-1 text-text-tertiary system-xs-regular">{t('modelProvider.emptyProviderTip', { ns: 'common' })}</div>
</div>
)}
{!!filteredConfiguredProviders?.length && (
@@ -145,7 +139,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
)}
{!!filteredNotConfiguredProviders?.length && (
<>
<div className="system-md-semibold mb-2 flex items-center pt-2 text-text-primary">{t('modelProvider.toBeConfigured', { ns: 'common' })}</div>
<div className="mb-2 flex items-center pt-2 text-text-primary system-md-semibold">{t('modelProvider.toBeConfigured', { ns: 'common' })}</div>
<div className="relative">
{filteredNotConfiguredProviders?.map(provider => (
<ProviderAddedCard

View File

@@ -48,14 +48,12 @@ const providerKeyToPluginId: Record<ModelProviderQuotaGetPaid, string> = {
type QuotaPanelProps = {
providers: ModelProvider[]
isLoading?: boolean
}
const QuotaPanel: FC<QuotaPanelProps> = ({
providers,
isLoading = false,
}) => {
const { t } = useTranslation()
const { currentWorkspace } = useAppContext()
const { currentWorkspace, isLoadingCurrentWorkspace } = useAppContext()
const { trial_models } = useGlobalPublicStore(s => s.systemFeatures)
const credits = Math.max((currentWorkspace.trial_credits - currentWorkspace.trial_credits_used) || 0, 0)
const providerMap = useMemo(() => new Map(
@@ -98,7 +96,7 @@ const QuotaPanel: FC<QuotaPanelProps> = ({
}
}, [providers, isShowInstallModal, hideInstallFromMarketplace])
if (isLoading) {
if (isLoadingCurrentWorkspace) {
return (
<div className="my-2 flex min-h-[72px] items-center justify-center rounded-xl border-[0.5px] border-components-panel-border bg-third-party-model-bg-default shadow-xs">
<Loading />
@@ -106,15 +104,18 @@ const QuotaPanel: FC<QuotaPanelProps> = ({
)
}
if (!currentWorkspace.id)
return null
return (
<div className={cn('my-2 min-w-[72px] shrink-0 rounded-xl border-[0.5px] pb-2.5 pl-4 pr-2.5 pt-3 shadow-xs', credits <= 0 ? 'border-state-destructive-border hover:bg-state-destructive-hover' : 'border-components-panel-border bg-third-party-model-bg-default')}>
<div className="system-xs-medium-uppercase mb-2 flex h-4 items-center text-text-tertiary">
<div className="mb-2 flex h-4 items-center text-text-tertiary system-xs-medium-uppercase">
{t('modelProvider.quota', { ns: 'common' })}
<Tooltip popupContent={t('modelProvider.card.tip', { ns: 'common', modelNames: trial_models.map(key => modelNameMap[key as keyof typeof modelNameMap]).filter(Boolean).join(', ') })} />
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 text-xs text-text-tertiary">
<span className="system-md-semibold-uppercase mr-0.5 text-text-secondary">{formatNumber(credits)}</span>
<span className="mr-0.5 text-text-secondary system-md-semibold-uppercase">{formatNumber(credits)}</span>
<span>{t('modelProvider.credits', { ns: 'common' })}</span>
{currentWorkspace.next_credit_reset_date
? (

View File

@@ -57,7 +57,7 @@ export default function CheckCode() {
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
}
else {
const redirectUrl = resolvePostLoginRedirect()
const redirectUrl = resolvePostLoginRedirect(searchParams)
router.replace(redirectUrl || '/apps')
}
}
@@ -95,8 +95,8 @@ export default function CheckCode() {
<RiMailSendFill className="h-6 w-6 text-2xl text-text-accent-light-mode-only" />
</div>
<div className="pb-4 pt-2">
<h2 className="text-text-primary title-4xl-semi-bold">{t('checkCode.checkYourEmail', { ns: 'login' })}</h2>
<p className="mt-2 text-text-secondary body-md-regular">
<h2 className="title-4xl-semi-bold text-text-primary">{t('checkCode.checkYourEmail', { ns: 'login' })}</h2>
<p className="body-md-regular mt-2 text-text-secondary">
<span>
{t('checkCode.tipsPrefix', { ns: 'login' })}
<strong>{email}</strong>
@@ -107,7 +107,7 @@ export default function CheckCode() {
</div>
<form onSubmit={handleSubmit}>
<label htmlFor="code" className="mb-1 text-text-secondary system-md-semibold">{t('checkCode.verificationCode', { ns: 'login' })}</label>
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('checkCode.verificationCode', { ns: 'login' })}</label>
<Input
ref={codeInputRef}
id="code"
@@ -127,7 +127,7 @@ export default function CheckCode() {
<div className="inline-block rounded-full bg-background-default-dimmed p-1">
<RiArrowLeftLine size={12} />
</div>
<span className="ml-2 system-xs-regular">{t('back', { ns: 'login' })}</span>
<span className="system-xs-regular ml-2">{t('back', { ns: 'login' })}</span>
</div>
</div>
)

View File

@@ -78,7 +78,7 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
}
else {
const redirectUrl = resolvePostLoginRedirect()
const redirectUrl = resolvePostLoginRedirect(searchParams)
router.replace(redirectUrl || '/apps')
}
}
@@ -105,7 +105,7 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
return (
<form onSubmit={noop}>
<div className="mb-3">
<label htmlFor="email" className="my-2 text-text-secondary system-md-semibold">
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
{t('email', { ns: 'login' })}
</label>
<div className="mt-1">
@@ -124,7 +124,7 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
<div className="mb-3">
<label htmlFor="password" className="my-2 flex items-center justify-between">
<span className="text-text-secondary system-md-semibold">{t('password', { ns: 'login' })}</span>
<span className="system-md-semibold text-text-secondary">{t('password', { ns: 'login' })}</span>
<Link
href={`/reset-password?${searchParams.toString()}`}
className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`}

View File

@@ -56,7 +56,7 @@ export default function InviteSettingsPage() {
if (res.result === 'success') {
// Tokens are now stored in cookies by the backend
await setLocaleOnClient(language, false)
const redirectUrl = resolvePostLoginRedirect()
const redirectUrl = resolvePostLoginRedirect(searchParams)
router.replace(redirectUrl || '/apps')
}
}
@@ -72,7 +72,7 @@ export default function InviteSettingsPage() {
<div className="flex flex-col md:w-[400px]">
<div className="mx-auto w-full">
<div className="mb-3 flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle text-2xl font-bold shadow-lg">🤷</div>
<h2 className="text-text-primary title-4xl-semi-bold">{t('invalid', { ns: 'login' })}</h2>
<h2 className="title-4xl-semi-bold text-text-primary">{t('invalid', { ns: 'login' })}</h2>
</div>
<div className="mx-auto mt-6 w-full">
<Button variant="primary" className="w-full !text-sm">
@@ -89,11 +89,11 @@ export default function InviteSettingsPage() {
<RiAccountCircleLine className="h-6 w-6 text-2xl text-text-accent-light-mode-only" />
</div>
<div className="pb-4 pt-2">
<h2 className="text-text-primary title-4xl-semi-bold">{t('setYourAccount', { ns: 'login' })}</h2>
<h2 className="title-4xl-semi-bold text-text-primary">{t('setYourAccount', { ns: 'login' })}</h2>
</div>
<form onSubmit={noop}>
<div className="mb-5">
<label htmlFor="name" className="my-2 text-text-secondary system-md-semibold">
<label htmlFor="name" className="system-md-semibold my-2 text-text-secondary">
{t('name', { ns: 'login' })}
</label>
<div className="mt-1">
@@ -114,7 +114,7 @@ export default function InviteSettingsPage() {
</div>
</div>
<div className="mb-5">
<label htmlFor="name" className="my-2 text-text-secondary system-md-semibold">
<label htmlFor="name" className="system-md-semibold my-2 text-text-secondary">
{t('interfaceLanguage', { ns: 'login' })}
</label>
<div className="mt-1">
@@ -129,7 +129,7 @@ export default function InviteSettingsPage() {
</div>
{/* timezone */}
<div className="mb-5">
<label htmlFor="timezone" className="text-text-secondary system-md-semibold">
<label htmlFor="timezone" className="system-md-semibold text-text-secondary">
{t('timezone', { ns: 'login' })}
</label>
<div className="mt-1">
@@ -153,11 +153,11 @@ export default function InviteSettingsPage() {
</div>
</form>
{!systemFeatures.branding.enabled && (
<div className="mt-2 block w-full text-text-tertiary system-xs-regular">
<div className="system-xs-regular mt-2 block w-full text-text-tertiary">
{t('license.tip', { ns: 'login' })}
&nbsp;
<Link
className="text-text-accent-secondary system-xs-medium"
className="system-xs-medium text-text-accent-secondary"
target="_blank"
rel="noopener noreferrer"
href={LICENSE_LINK}

View File

@@ -42,7 +42,7 @@ const NormalForm = () => {
try {
if (isLoggedIn) {
setIsRedirecting(true)
const redirectUrl = resolvePostLoginRedirect()
const redirectUrl = resolvePostLoginRedirect(searchParams)
router.replace(redirectUrl || '/apps')
return
}
@@ -98,8 +98,8 @@ const NormalForm = () => {
<RiContractLine className="h-5 w-5" />
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
</div>
<p className="text-text-primary system-sm-medium">{t('licenseLost', { ns: 'login' })}</p>
<p className="mt-1 text-text-tertiary system-xs-regular">{t('licenseLostTip', { ns: 'login' })}</p>
<p className="system-sm-medium text-text-primary">{t('licenseLost', { ns: 'login' })}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('licenseLostTip', { ns: 'login' })}</p>
</div>
</div>
</div>
@@ -114,8 +114,8 @@ const NormalForm = () => {
<RiContractLine className="h-5 w-5" />
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
</div>
<p className="text-text-primary system-sm-medium">{t('licenseExpired', { ns: 'login' })}</p>
<p className="mt-1 text-text-tertiary system-xs-regular">{t('licenseExpiredTip', { ns: 'login' })}</p>
<p className="system-sm-medium text-text-primary">{t('licenseExpired', { ns: 'login' })}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('licenseExpiredTip', { ns: 'login' })}</p>
</div>
</div>
</div>
@@ -130,8 +130,8 @@ const NormalForm = () => {
<RiContractLine className="h-5 w-5" />
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
</div>
<p className="text-text-primary system-sm-medium">{t('licenseInactive', { ns: 'login' })}</p>
<p className="mt-1 text-text-tertiary system-xs-regular">{t('licenseInactiveTip', { ns: 'login' })}</p>
<p className="system-sm-medium text-text-primary">{t('licenseInactive', { ns: 'login' })}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('licenseInactiveTip', { ns: 'login' })}</p>
</div>
</div>
</div>
@@ -144,12 +144,12 @@ const NormalForm = () => {
{isInviteLink
? (
<div className="mx-auto w-full">
<h2 className="text-text-primary title-4xl-semi-bold">
<h2 className="title-4xl-semi-bold text-text-primary">
{t('join', { ns: 'login' })}
{workspaceName}
</h2>
{!systemFeatures.branding.enabled && (
<p className="mt-2 text-text-tertiary body-md-regular">
<p className="body-md-regular mt-2 text-text-tertiary">
{t('joinTipStart', { ns: 'login' })}
{workspaceName}
{t('joinTipEnd', { ns: 'login' })}
@@ -159,8 +159,8 @@ const NormalForm = () => {
)
: (
<div className="mx-auto w-full">
<h2 className="text-text-primary title-4xl-semi-bold">{systemFeatures.branding.enabled ? t('pageTitleForE', { ns: 'login' }) : t('pageTitle', { ns: 'login' })}</h2>
<p className="mt-2 text-text-tertiary body-md-regular">{t('welcome', { ns: 'login' })}</p>
<h2 className="title-4xl-semi-bold text-text-primary">{systemFeatures.branding.enabled ? t('pageTitleForE', { ns: 'login' }) : t('pageTitle', { ns: 'login' })}</h2>
<p className="body-md-regular mt-2 text-text-tertiary">{t('welcome', { ns: 'login' })}</p>
</div>
)}
<div className="relative">
@@ -177,7 +177,7 @@ const NormalForm = () => {
<div className="relative mt-6">
<div className="flex items-center">
<div className="h-px flex-1 bg-gradient-to-r from-background-gradient-mask-transparent to-divider-regular"></div>
<span className="px-3 text-text-tertiary system-xs-medium-uppercase">{t('or', { ns: 'login' })}</span>
<span className="system-xs-medium-uppercase px-3 text-text-tertiary">{t('or', { ns: 'login' })}</span>
<div className="h-px flex-1 bg-gradient-to-l from-background-gradient-mask-transparent to-divider-regular"></div>
</div>
</div>
@@ -190,7 +190,7 @@ const NormalForm = () => {
<MailAndCodeAuth isInvite={isInviteLink} />
{systemFeatures.enable_email_password_login && (
<div className="cursor-pointer py-1 text-center" onClick={() => { updateAuthType('password') }}>
<span className="text-components-button-secondary-accent-text system-xs-medium">{t('usePassword', { ns: 'login' })}</span>
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('usePassword', { ns: 'login' })}</span>
</div>
)}
</>
@@ -200,7 +200,7 @@ const NormalForm = () => {
<MailAndPasswordAuth isInvite={isInviteLink} isEmailSetup={systemFeatures.is_email_setup} allowRegistration={systemFeatures.is_allow_register} />
{systemFeatures.enable_email_code_login && (
<div className="cursor-pointer py-1 text-center" onClick={() => { updateAuthType('code') }}>
<span className="text-components-button-secondary-accent-text system-xs-medium">{t('useVerificationCode', { ns: 'login' })}</span>
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('useVerificationCode', { ns: 'login' })}</span>
</div>
)}
</>
@@ -227,8 +227,8 @@ const NormalForm = () => {
<div className="shadows-shadow-lg mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow">
<RiDoorLockLine className="h-5 w-5" />
</div>
<p className="text-text-primary system-sm-medium">{t('noLoginMethod', { ns: 'login' })}</p>
<p className="mt-1 text-text-tertiary system-xs-regular">{t('noLoginMethodTip', { ns: 'login' })}</p>
<p className="system-sm-medium text-text-primary">{t('noLoginMethod', { ns: 'login' })}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('noLoginMethodTip', { ns: 'login' })}</p>
</div>
<div className="relative my-2 py-2">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
@@ -239,11 +239,11 @@ const NormalForm = () => {
)}
{!systemFeatures.branding.enabled && (
<>
<div className="mt-2 block w-full text-text-tertiary system-xs-regular">
<div className="system-xs-regular mt-2 block w-full text-text-tertiary">
{t('tosDesc', { ns: 'login' })}
&nbsp;
<Link
className="text-text-secondary system-xs-medium hover:underline"
className="system-xs-medium text-text-secondary hover:underline"
target="_blank"
rel="noopener noreferrer"
href="https://dify.ai/terms"
@@ -252,7 +252,7 @@ const NormalForm = () => {
</Link>
&nbsp;&&nbsp;
<Link
className="text-text-secondary system-xs-medium hover:underline"
className="system-xs-medium text-text-secondary hover:underline"
target="_blank"
rel="noopener noreferrer"
href="https://dify.ai/privacy"
@@ -261,11 +261,11 @@ const NormalForm = () => {
</Link>
</div>
{IS_CE_EDITION && (
<div className="w-hull mt-2 block text-text-tertiary system-xs-regular">
<div className="w-hull system-xs-regular mt-2 block text-text-tertiary">
{t('goToInit', { ns: 'login' })}
&nbsp;
<Link
className="text-text-secondary system-xs-medium hover:underline"
className="system-xs-medium text-text-secondary hover:underline"
href="/install"
>
{t('setAdminAccount', { ns: 'login' })}

View File

@@ -1,15 +1,37 @@
let postLoginRedirect: string | null = null
import type { ReadonlyURLSearchParams } from 'next/navigation'
import dayjs from 'dayjs'
import { OAUTH_AUTHORIZE_PENDING_KEY, REDIRECT_URL_KEY } from '@/app/account/oauth/authorize/constants'
export const setPostLoginRedirect = (value: string | null) => {
postLoginRedirect = value
function getItemWithExpiry(key: string): string | null {
const itemStr = localStorage.getItem(key)
if (!itemStr)
return null
try {
const item = JSON.parse(itemStr)
localStorage.removeItem(key)
if (!item?.value)
return null
return dayjs().unix() > item.expiry ? null : item.value
}
catch {
return null
}
}
export const resolvePostLoginRedirect = () => {
if (postLoginRedirect) {
const redirectUrl = postLoginRedirect
postLoginRedirect = null
return redirectUrl
export const resolvePostLoginRedirect = (searchParams: ReadonlyURLSearchParams) => {
const redirectUrl = searchParams.get(REDIRECT_URL_KEY)
if (redirectUrl) {
try {
localStorage.removeItem(OAUTH_AUTHORIZE_PENDING_KEY)
return decodeURIComponent(redirectUrl)
}
catch (e) {
console.error('Failed to decode redirect URL:', e)
return redirectUrl
}
}
return null
return getItemWithExpiry(OAUTH_AUTHORIZE_PENDING_KEY)
}

View File

@@ -26,11 +26,9 @@ export type AppContextValue = {
isCurrentWorkspaceOwner: boolean
isCurrentWorkspaceEditor: boolean
isCurrentWorkspaceDatasetOperator: boolean
mutateCurrentWorkspace: VoidFunction
langGeniusVersionInfo: LangGeniusVersionResponse
useSelector: typeof useSelector
isLoadingCurrentWorkspace: boolean
isValidatingCurrentWorkspace: boolean
}
const userProfilePlaceholder = {
@@ -73,11 +71,9 @@ const AppContext = createContext<AppContextValue>({
isCurrentWorkspaceEditor: false,
isCurrentWorkspaceDatasetOperator: false,
mutateUserProfile: noop,
mutateCurrentWorkspace: noop,
langGeniusVersionInfo: initialLangGeniusVersionInfo,
useSelector,
isLoadingCurrentWorkspace: false,
isValidatingCurrentWorkspace: false,
})
export function useSelector<T>(selector: (value: AppContextValue) => T): T {
@@ -92,7 +88,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
const queryClient = useQueryClient()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { data: userProfileResp } = useUserProfile()
const { data: currentWorkspaceResp, isPending: isLoadingCurrentWorkspace, isFetching: isValidatingCurrentWorkspace } = useCurrentWorkspace()
const { data: currentWorkspaceResp, isPending: isLoadingCurrentWorkspace } = useCurrentWorkspace()
const langGeniusVersionQuery = useLangGeniusVersion(
userProfileResp?.meta.currentVersion,
!systemFeatures.branding.enabled,
@@ -124,10 +120,6 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
queryClient.invalidateQueries({ queryKey: ['common', 'user-profile'] })
}, [queryClient])
const mutateCurrentWorkspace = useCallback(() => {
queryClient.invalidateQueries({ queryKey: ['common', 'current-workspace'] })
}, [queryClient])
// #region Zendesk conversation fields
useEffect(() => {
if (ZENDESK_FIELD_IDS.ENVIRONMENT && langGeniusVersionInfo?.current_env) {
@@ -199,9 +191,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
isCurrentWorkspaceOwner,
isCurrentWorkspaceEditor,
isCurrentWorkspaceDatasetOperator,
mutateCurrentWorkspace,
isLoadingCurrentWorkspace,
isValidatingCurrentWorkspace,
}}
>
<div className="flex h-full flex-col overflow-y-auto">

View File

@@ -278,6 +278,9 @@
}
},
"app/account/oauth/authorize/page.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 4
},
"ts/no-explicit-any": {
"count": 1
}
@@ -3073,12 +3076,6 @@
}
},
"app/components/custom/custom-web-app-brand/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 12
},
"tailwindcss/no-unnecessary-whitespace": {
"count": 1
},
"ts/no-explicit-any": {
"count": 2
}
@@ -4515,11 +4512,6 @@
"count": 3
}
},
"app/components/header/account-setting/model-provider-page/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 5
}
},
"app/components/header/account-setting/model-provider-page/install-from-marketplace.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 3
@@ -4723,11 +4715,6 @@
"count": 3
}
},
"app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
},
"app/components/header/account-setting/model-provider-page/provider-icon/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
@@ -8590,16 +8577,29 @@
"count": 6
}
},
"app/signin/check-code/page.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 4
}
},
"app/signin/components/mail-and-code-auth.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
},
"app/signin/components/mail-and-password-auth.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 2
},
"ts/no-explicit-any": {
"count": 1
}
},
"app/signin/invite-settings/page.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 7
}
},
"app/signin/layout.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
@@ -8608,6 +8608,11 @@
"count": 1
}
},
"app/signin/normal-form.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 20
}
},
"app/signin/one-more-step.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 7

View File

@@ -1,7 +1,7 @@
{
"name": "dify-web",
"type": "module",
"version": "1.13.0",
"version": "1.12.1",
"private": true,
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a",
"imports": {
@@ -23,7 +23,7 @@
"and_qq >= 14.9"
],
"engines": {
"node": "^22"
"node": ">=24"
},
"scripts": {
"dev": "next dev",

View File

@@ -22,7 +22,7 @@ import type {
UserProfileResponse,
} from '@/models/common'
import type { RETRIEVE_METHOD } from '@/types/app'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { IS_DEV } from '@/config'
import { get, post } from './base'
import { useInvalid } from './use-base'
@@ -318,6 +318,15 @@ export const useInvalidDataSourceIntegrates = () => {
return useInvalid(commonQueryKeys.dataSourceIntegrates)
}
export const useInvalidateCurrentWorkspace = () => {
const queryClient = useQueryClient()
return () => {
queryClient.invalidateQueries({
queryKey: commonQueryKeys.currentWorkspace,
})
}
}
export const usePluginProviders = () => {
return useQuery<PluginProvider[] | null>({
queryKey: commonQueryKeys.pluginProviders,