From b4e2af96e2c1318e2d7b389abdc1596f9ef273e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:17:04 +0800 Subject: [PATCH 1/6] chore(deps): bump @lexical/utils from 0.38.2 to 0.39.0 in /web (#31503) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package.json | 2 +- web/pnpm-lock.yaml | 80 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/web/package.json b/web/package.json index 69cc08bd32..48a6795d83 100644 --- a/web/package.json +++ b/web/package.json @@ -67,7 +67,7 @@ "@lexical/react": "0.38.2", "@lexical/selection": "0.38.2", "@lexical/text": "0.38.2", - "@lexical/utils": "0.38.2", + "@lexical/utils": "0.39.0", "@monaco-editor/react": "4.7.0", "@octokit/core": "6.1.6", "@octokit/request-error": "6.1.8", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index c98b57ee48..d5a8b529ad 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -94,8 +94,8 @@ importers: specifier: 0.38.2 version: 0.38.2 '@lexical/utils': - specifier: 0.38.2 - version: 0.38.2 + specifier: 0.39.0 + version: 0.39.0 '@monaco-editor/react': specifier: 4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -2066,6 +2066,9 @@ packages: '@lexical/clipboard@0.38.2': resolution: {integrity: sha512-dDShUplCu8/o6BB9ousr3uFZ9bltR+HtleF/Tl8FXFNPpZ4AXhbLKUoJuucRuIr+zqT7RxEv/3M6pk/HEoE6NQ==} + '@lexical/clipboard@0.39.0': + resolution: {integrity: sha512-ylrHy8M+I5EH4utwqivslugqQhvgLTz9VEJdrb2RjbhKQEXwMcqKCRWh6cRfkYx64onE2YQE0nRIdzHhExEpLQ==} + '@lexical/code@0.38.2': resolution: {integrity: sha512-wpqgbmPsfi/+8SYP0zI2kml09fGPRhzO5litR9DIbbSGvcbawMbRNcKLO81DaTbsJRnBJiQvbBBBJAwZKRqgBw==} @@ -2081,6 +2084,9 @@ packages: '@lexical/extension@0.38.2': resolution: {integrity: sha512-qbUNxEVjAC0kxp7hEMTzktj0/51SyJoIJWK6Gm790b4yNBq82fEPkksfuLkRg9VQUteD0RT1Nkjy8pho8nNamw==} + '@lexical/extension@0.39.0': + resolution: {integrity: sha512-mp/WcF8E53FWPiUHgHQz382J7u7C4+cELYNkC00dKaymf8NhS6M65Y8tyDikNGNUcLXSzaluwK0HkiKjTYGhVQ==} + '@lexical/hashtag@0.38.2': resolution: {integrity: sha512-jNI4Pv+plth39bjOeeQegMypkjDmoMWBMZtV0lCynBpkkPFlfMnyL9uzW/IxkZnX8LXWSw5mbWk07nqOUNTCrA==} @@ -2090,12 +2096,18 @@ packages: '@lexical/html@0.38.2': resolution: {integrity: sha512-pC5AV+07bmHistRwgG3NJzBMlIzSdxYO6rJU4eBNzyR4becdiLsI4iuv+aY7PhfSv+SCs7QJ9oc4i5caq48Pkg==} + '@lexical/html@0.39.0': + resolution: {integrity: sha512-7VLWP5DpzBg3kKctpNK6PbhymKAtU6NAnKieopCfCIWlMW+EqpldteiIXGqSqrMRK0JWTmF1gKgr9nnQyOOsXw==} + '@lexical/link@0.38.2': resolution: {integrity: sha512-UOKTyYqrdCR9+7GmH6ZVqJTmqYefKGMUHMGljyGks+OjOGZAQs78S1QgcPEqltDy+SSdPSYK7wAo6gjxZfEq9g==} '@lexical/list@0.38.2': resolution: {integrity: sha512-OQm9TzatlMrDZGxMxbozZEHzMJhKxAbH1TOnOGyFfzpfjbnFK2y8oLeVsfQZfZRmiqQS4Qc/rpFnRP2Ax5dsbA==} + '@lexical/list@0.39.0': + resolution: {integrity: sha512-mxgSxUrakTCHtC+gF30BChQBJTsCMiMgfC2H5VvhcFwXMgsKE/aK9+a+C/sSvvzCmPXqzYsuAcGkJcrY3e5xlw==} + '@lexical/mark@0.38.2': resolution: {integrity: sha512-U+8KGwc3cP5DxSs15HfkP2YZJDs5wMbWQAwpGqep9bKphgxUgjPViKhdi+PxIt2QEzk7WcoZWUsK1d2ty/vSmg==} @@ -2123,15 +2135,24 @@ packages: '@lexical/selection@0.38.2': resolution: {integrity: sha512-eMFiWlBH6bEX9U9sMJ6PXPxVXTrihQfFeiIlWLuTpEIDF2HRz7Uo1KFRC/yN6q0DQaj7d9NZYA6Mei5DoQuz5w==} + '@lexical/selection@0.39.0': + resolution: {integrity: sha512-j0cgNuTKDCdf/4MzRnAUwEqG6C/WQp18k2WKmX5KIVZJlhnGIJmlgSBrxjo8AuZ16DIHxTm2XNB4cUDCgZNuPA==} + '@lexical/table@0.38.2': resolution: {integrity: sha512-uu0i7yz0nbClmHOO5ZFsinRJE6vQnFz2YPblYHAlNigiBedhqMwSv5bedrzDq8nTTHwych3mC63tcyKIrM+I1g==} + '@lexical/table@0.39.0': + resolution: {integrity: sha512-1eH11kV4bJ0fufCYl8DpE19kHwqUI8Ev5CZwivfAtC3ntwyNkeEpjCc0pqeYYIWN/4rTZ5jgB3IJV4FntyfCzw==} + '@lexical/text@0.38.2': resolution: {integrity: sha512-+juZxUugtC4T37aE3P0l4I9tsWbogDUnTI/mgYk4Ht9g+gLJnhQkzSA8chIyfTxbj5i0A8yWrUUSw+/xA7lKUQ==} '@lexical/utils@0.38.2': resolution: {integrity: sha512-y+3rw15r4oAWIEXicUdNjfk8018dbKl7dWHqGHVEtqzAYefnEYdfD2FJ5KOTXfeoYfxi8yOW7FvzS4NZDi8Bfw==} + '@lexical/utils@0.39.0': + resolution: {integrity: sha512-8YChidpMJpwQc4nex29FKUeuZzC++QCS/Jt46lPuy1GS/BZQoPHFKQ5hyVvM9QVhc5CEs4WGNoaCZvZIVN8bQw==} + '@lexical/yjs@0.38.2': resolution: {integrity: sha512-fg6ZHNrVQmy1AAxaTs8HrFbeNTJCaCoEDPi6pqypHQU3QVfqr4nq0L0EcHU/TRlR1CeduEPvZZIjUUxWTZ0u8g==} peerDependencies: @@ -2619,6 +2640,9 @@ packages: '@preact/signals-core@1.12.1': resolution: {integrity: sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA==} + '@preact/signals-core@1.12.2': + resolution: {integrity: sha512-5Yf8h1Ke3SMHr15xl630KtwPTW4sYDFkkxS0vQ8UiQLWwZQnrF9IKaVG1mN5VcJz52EcWs2acsc/Npjha/7ysA==} + '@preact/signals@1.3.2': resolution: {integrity: sha512-naxcJgUJ6BTOROJ7C3QML7KvwKwCXQJYTc5L/b0eEsdYgPB6SxwoQ1vDGcS0Q7GVjAenVq/tXrybVdFShHYZWg==} peerDependencies: @@ -6223,6 +6247,9 @@ packages: lexical@0.38.2: resolution: {integrity: sha512-JJmfsG3c4gwBHzUGffbV7ifMNkKAWMCnYE3xJl87gty7hjyV5f3xq7eqTjP5HFYvO4XpjJvvWO2/djHp5S10tw==} + lexical@0.39.0: + resolution: {integrity: sha512-lpLv7MEJH5QDujEDlYqettL3ATVtNYjqyimzqgrm0RvCm3AO9WXSdsgTxuN7IAZRu88xkxCDeYubeUf4mNZVdg==} + lib0@0.2.117: resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} engines: {node: '>=16'} @@ -10373,6 +10400,14 @@ snapshots: '@lexical/utils': 0.38.2 lexical: 0.38.2 + '@lexical/clipboard@0.39.0': + dependencies: + '@lexical/html': 0.39.0 + '@lexical/list': 0.39.0 + '@lexical/selection': 0.39.0 + '@lexical/utils': 0.39.0 + lexical: 0.39.0 + '@lexical/code@0.38.2': dependencies: '@lexical/utils': 0.38.2 @@ -10401,6 +10436,12 @@ snapshots: '@preact/signals-core': 1.12.1 lexical: 0.38.2 + '@lexical/extension@0.39.0': + dependencies: + '@lexical/utils': 0.39.0 + '@preact/signals-core': 1.12.2 + lexical: 0.39.0 + '@lexical/hashtag@0.38.2': dependencies: '@lexical/text': 0.38.2 @@ -10419,6 +10460,12 @@ snapshots: '@lexical/utils': 0.38.2 lexical: 0.38.2 + '@lexical/html@0.39.0': + dependencies: + '@lexical/selection': 0.39.0 + '@lexical/utils': 0.39.0 + lexical: 0.39.0 + '@lexical/link@0.38.2': dependencies: '@lexical/extension': 0.38.2 @@ -10432,6 +10479,13 @@ snapshots: '@lexical/utils': 0.38.2 lexical: 0.38.2 + '@lexical/list@0.39.0': + dependencies: + '@lexical/extension': 0.39.0 + '@lexical/selection': 0.39.0 + '@lexical/utils': 0.39.0 + lexical: 0.39.0 + '@lexical/mark@0.38.2': dependencies: '@lexical/utils': 0.38.2 @@ -10501,6 +10555,10 @@ snapshots: dependencies: lexical: 0.38.2 + '@lexical/selection@0.39.0': + dependencies: + lexical: 0.39.0 + '@lexical/table@0.38.2': dependencies: '@lexical/clipboard': 0.38.2 @@ -10508,6 +10566,13 @@ snapshots: '@lexical/utils': 0.38.2 lexical: 0.38.2 + '@lexical/table@0.39.0': + dependencies: + '@lexical/clipboard': 0.39.0 + '@lexical/extension': 0.39.0 + '@lexical/utils': 0.39.0 + lexical: 0.39.0 + '@lexical/text@0.38.2': dependencies: lexical: 0.38.2 @@ -10519,6 +10584,13 @@ snapshots: '@lexical/table': 0.38.2 lexical: 0.38.2 + '@lexical/utils@0.39.0': + dependencies: + '@lexical/list': 0.39.0 + '@lexical/selection': 0.39.0 + '@lexical/table': 0.39.0 + lexical: 0.39.0 + '@lexical/yjs@0.38.2(yjs@13.6.27)': dependencies: '@lexical/offset': 0.38.2 @@ -10973,6 +11045,8 @@ snapshots: '@preact/signals-core@1.12.1': {} + '@preact/signals-core@1.12.2': {} + '@preact/signals@1.3.2(preact@10.28.0)': dependencies: '@preact/signals-core': 1.12.1 @@ -15090,6 +15164,8 @@ snapshots: lexical@0.38.2: {} + lexical@0.39.0: {} + lib0@0.2.117: dependencies: isomorphic.js: 0.2.5 From b9f1d65d4fb601bf5b80eb9bb7df9e11b38fcc95 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Mon, 26 Jan 2026 11:23:38 +0900 Subject: [PATCH 2/6] refactor: example of refine dict / Mapping (#31498) --- api/controllers/console/app/workflow.py | 6 ++++-- api/core/app/apps/advanced_chat/app_generator.py | 13 +++++++++---- api/core/app/apps/workflow/app_generator.py | 13 +++++++++---- api/services/app_generate_service.py | 11 +++++++++-- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index b4f2ef0ba8..acaf85a6b1 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -470,7 +470,7 @@ class AdvancedChatDraftRunLoopNodeApi(Resource): Run draft workflow loop node """ current_user, _ = current_account_with_tenant() - args = LoopNodeRunPayload.model_validate(console_ns.payload or {}).model_dump(exclude_none=True) + args = LoopNodeRunPayload.model_validate(console_ns.payload or {}) try: response = AppGenerateService.generate_single_loop( @@ -508,7 +508,7 @@ class WorkflowDraftRunLoopNodeApi(Resource): Run draft workflow loop node """ current_user, _ = current_account_with_tenant() - args = LoopNodeRunPayload.model_validate(console_ns.payload or {}).model_dump(exclude_none=True) + args = LoopNodeRunPayload.model_validate(console_ns.payload or {}) try: response = AppGenerateService.generate_single_loop( @@ -999,6 +999,7 @@ class DraftWorkflowTriggerRunApi(Resource): if not event: return jsonable_encoder({"status": "waiting", "retry_in": LISTENING_RETRY_IN}) workflow_args = dict(event.workflow_args) + workflow_args[SKIP_PREPARE_USER_INPUTS_KEY] = True return helper.compact_generate_response( AppGenerateService.generate( @@ -1147,6 +1148,7 @@ class DraftWorkflowTriggerRunAllApi(Resource): try: workflow_args = dict(trigger_debug_event.workflow_args) + workflow_args[SKIP_PREPARE_USER_INPUTS_KEY] = True response = AppGenerateService.generate( app_model=app_model, diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index feb0d3358c..528c45f6c8 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import contextvars import logging import threading import uuid from collections.abc import Generator, Mapping -from typing import Any, Literal, Union, overload +from typing import TYPE_CHECKING, Any, Literal, Union, overload from flask import Flask, current_app from pydantic import ValidationError @@ -13,6 +15,9 @@ from sqlalchemy.orm import Session, sessionmaker import contexts from configs import dify_config from constants import UUID_NIL + +if TYPE_CHECKING: + from controllers.console.app.workflow import LoopNodeRunPayload from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager from core.app.apps.advanced_chat.app_runner import AdvancedChatAppRunner @@ -304,7 +309,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): workflow: Workflow, node_id: str, user: Account | EndUser, - args: Mapping, + args: LoopNodeRunPayload, streaming: bool = True, ) -> Mapping[str, Any] | Generator[str | Mapping[str, Any], Any, None]: """ @@ -320,7 +325,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): if not node_id: raise ValueError("node_id is required") - if args.get("inputs") is None: + if args.inputs is None: raise ValueError("inputs is required") # convert to app config @@ -338,7 +343,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): stream=streaming, invoke_from=InvokeFrom.DEBUGGER, extras={"auto_generate_conversation_name": False}, - single_loop_run=AdvancedChatAppGenerateEntity.SingleLoopRunEntity(node_id=node_id, inputs=args["inputs"]), + single_loop_run=AdvancedChatAppGenerateEntity.SingleLoopRunEntity(node_id=node_id, inputs=args.inputs), ) contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers_lock.set(threading.Lock()) diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index 2be773f103..ee205ed153 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import contextvars import logging import threading import uuid from collections.abc import Generator, Mapping, Sequence -from typing import Any, Literal, Union, overload +from typing import TYPE_CHECKING, Any, Literal, Union, overload from flask import Flask, current_app from pydantic import ValidationError @@ -40,6 +42,9 @@ from models import Account, App, EndUser, Workflow, WorkflowNodeExecutionTrigger from models.enums import WorkflowRunTriggeredFrom from services.workflow_draft_variable_service import DraftVarLoader, WorkflowDraftVariableService +if TYPE_CHECKING: + from controllers.console.app.workflow import LoopNodeRunPayload + SKIP_PREPARE_USER_INPUTS_KEY = "_skip_prepare_user_inputs" logger = logging.getLogger(__name__) @@ -381,7 +386,7 @@ class WorkflowAppGenerator(BaseAppGenerator): workflow: Workflow, node_id: str, user: Account | EndUser, - args: Mapping[str, Any], + args: LoopNodeRunPayload, streaming: bool = True, ) -> Mapping[str, Any] | Generator[str | Mapping[str, Any], None, None]: """ @@ -397,7 +402,7 @@ class WorkflowAppGenerator(BaseAppGenerator): if not node_id: raise ValueError("node_id is required") - if args.get("inputs") is None: + if args.inputs is None: raise ValueError("inputs is required") # convert to app config @@ -413,7 +418,7 @@ class WorkflowAppGenerator(BaseAppGenerator): stream=streaming, invoke_from=InvokeFrom.DEBUGGER, extras={"auto_generate_conversation_name": False}, - single_loop_run=WorkflowAppGenerateEntity.SingleLoopRunEntity(node_id=node_id, inputs=args["inputs"]), + single_loop_run=WorkflowAppGenerateEntity.SingleLoopRunEntity(node_id=node_id, inputs=args.inputs or {}), workflow_execution_id=str(uuid.uuid4()), ) contexts.plugin_tool_providers.set({}) diff --git a/api/services/app_generate_service.py b/api/services/app_generate_service.py index cc58899dc4..ce85f2e914 100644 --- a/api/services/app_generate_service.py +++ b/api/services/app_generate_service.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import uuid from collections.abc import Generator, Mapping -from typing import Any, Union +from typing import TYPE_CHECKING, Any, Union from configs import dify_config from core.app.apps.advanced_chat.app_generator import AdvancedChatAppGenerator @@ -18,6 +20,9 @@ from services.errors.app import QuotaExceededError, WorkflowIdFormatError, Workf from services.errors.llm import InvokeRateLimitError from services.workflow_service import WorkflowService +if TYPE_CHECKING: + from controllers.console.app.workflow import LoopNodeRunPayload + class AppGenerateService: @classmethod @@ -165,7 +170,9 @@ class AppGenerateService: raise ValueError(f"Invalid app mode {app_model.mode}") @classmethod - def generate_single_loop(cls, app_model: App, user: Account, node_id: str, args: Any, streaming: bool = True): + def generate_single_loop( + cls, app_model: App, user: Account, node_id: str, args: LoopNodeRunPayload, streaming: bool = True + ): if app_model.mode == AppMode.ADVANCED_CHAT: workflow = cls._get_workflow(app_model, InvokeFrom.DEBUGGER) return AdvancedChatAppGenerator.convert_to_event_stream( From 7c12e923b6cd5e7e082241e2add8266b426b56d4 Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Mon, 26 Jan 2026 11:52:05 +0800 Subject: [PATCH 3/6] feat: add trial model list in system features (#31313) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: hj24 --- api/enums/hosted_provider.py | 21 +++++++++++++++++++++ api/services/feature_service.py | 14 ++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 api/enums/hosted_provider.py diff --git a/api/enums/hosted_provider.py b/api/enums/hosted_provider.py new file mode 100644 index 0000000000..c6d3715dc1 --- /dev/null +++ b/api/enums/hosted_provider.py @@ -0,0 +1,21 @@ +from enum import StrEnum + + +class HostedTrialProvider(StrEnum): + """ + Enum representing hosted model provider names for trial access. + """ + + OPENAI = "langgenius/openai/openai" + ANTHROPIC = "langgenius/anthropic/anthropic" + GEMINI = "langgenius/gemini/google" + X = "langgenius/x/x" + DEEPSEEK = "langgenius/deepseek/deepseek" + TONGYI = "langgenius/tongyi/tongyi" + + @property + def config_key(self) -> str: + """Return the config key used in dify_config (e.g., HOSTED_{config_key}_PAID_ENABLED).""" + if self == HostedTrialProvider.X: + return "XAI" + return self.name diff --git a/api/services/feature_service.py b/api/services/feature_service.py index b2fb3784e8..d94ae49d91 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -4,6 +4,7 @@ from pydantic import BaseModel, ConfigDict, Field from configs import dify_config from enums.cloud_plan import CloudPlan +from enums.hosted_provider import HostedTrialProvider from services.billing_service import BillingService from services.enterprise.enterprise_service import EnterpriseService @@ -170,6 +171,7 @@ class SystemFeatureModel(BaseModel): plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel() enable_change_email: bool = True plugin_manager: PluginManagerModel = PluginManagerModel() + trial_models: list[str] = [] enable_trial_app: bool = False enable_explore_banner: bool = False @@ -227,9 +229,21 @@ class FeatureService: system_features.is_allow_register = dify_config.ALLOW_REGISTER system_features.is_allow_create_workspace = dify_config.ALLOW_CREATE_WORKSPACE system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != "" + system_features.trial_models = cls._fulfill_trial_models_from_env() system_features.enable_trial_app = dify_config.ENABLE_TRIAL_APP system_features.enable_explore_banner = dify_config.ENABLE_EXPLORE_BANNER + @classmethod + def _fulfill_trial_models_from_env(cls) -> list[str]: + return [ + provider.value + for provider in HostedTrialProvider + if ( + getattr(dify_config, f"HOSTED_{provider.config_key}_PAID_ENABLED", False) + and getattr(dify_config, f"HOSTED_{provider.config_key}_TRIAL_ENABLED", False) + ) + ] + @classmethod def _fulfill_params_from_env(cls, features: FeatureModel): features.can_replace_logo = dify_config.CAN_REPLACE_LOGO From a43d2ec4f022d4446732527ac6565ffe691f493f Mon Sep 17 00:00:00 2001 From: Coding On Star <447357187@qq.com> Date: Mon, 26 Jan 2026 14:03:51 +0800 Subject: [PATCH 4/6] refactor: restructure Completed component (#31435) Co-authored-by: CodingOnStar --- .../app/overview/settings/index.spec.tsx | 3 + .../create/website/watercrawl/index.spec.tsx | 3 + .../completed/child-segment-list.spec.tsx | 499 +++++ .../detail/completed/child-segment-list.tsx | 229 +- .../detail/completed/common/drawer.tsx | 93 +- .../detail/completed/common/empty.spec.tsx | 129 ++ .../completed/components/drawer-group.tsx | 151 ++ .../detail/completed/components/index.ts | 3 + .../detail/completed/components/menu-bar.tsx | 76 + .../components/segment-list-content.tsx | 127 ++ .../documents/detail/completed/hooks/index.ts | 14 + .../hooks/use-child-segment-data.spec.ts | 568 +++++ .../completed/hooks/use-child-segment-data.ts | 241 ++ .../detail/completed/hooks/use-modal-state.ts | 141 ++ .../completed/hooks/use-search-filter.ts | 85 + .../hooks/use-segment-list-data.spec.ts | 942 ++++++++ .../completed/hooks/use-segment-list-data.ts | 363 +++ .../completed/hooks/use-segment-selection.ts | 58 + .../documents/detail/completed/index.spec.tsx | 1863 ++++++++++++++++ .../documents/detail/completed/index.tsx | 871 ++------ .../detail/completed/segment-list-context.ts | 34 + .../skeleton/full-doc-list-skeleton.spec.tsx | 93 + web/app/components/tools/provider/detail.tsx | 3 +- .../workflow-tool/configure-button.spec.tsx | 1975 +++++++++++++++++ .../components/tools/workflow-tool/index.tsx | 26 +- web/eslint-suppressions.json | 13 - 26 files changed, 7751 insertions(+), 852 deletions(-) create mode 100644 web/app/components/datasets/documents/detail/completed/child-segment-list.spec.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/common/empty.spec.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/components/drawer-group.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/components/index.ts create mode 100644 web/app/components/datasets/documents/detail/completed/components/menu-bar.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/components/segment-list-content.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/index.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.spec.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-modal-state.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-search-filter.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-segment-list-data.spec.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-segment-list-data.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-segment-selection.ts create mode 100644 web/app/components/datasets/documents/detail/completed/index.spec.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/segment-list-context.ts create mode 100644 web/app/components/datasets/documents/detail/completed/skeleton/full-doc-list-skeleton.spec.tsx create mode 100644 web/app/components/tools/workflow-tool/configure-button.spec.tsx diff --git a/web/app/components/app/overview/settings/index.spec.tsx b/web/app/components/app/overview/settings/index.spec.tsx index 776c55d149..c9cbe0b724 100644 --- a/web/app/components/app/overview/settings/index.spec.tsx +++ b/web/app/components/app/overview/settings/index.spec.tsx @@ -1,3 +1,6 @@ +/** + * @vitest-environment jsdom + */ import type { ReactNode } from 'react' import type { ModalContextState } from '@/context/modal-context' import type { ProviderContextState } from '@/context/provider-context' diff --git a/web/app/components/datasets/create/website/watercrawl/index.spec.tsx b/web/app/components/datasets/create/website/watercrawl/index.spec.tsx index 4bb8267cea..646c59eb75 100644 --- a/web/app/components/datasets/create/website/watercrawl/index.spec.tsx +++ b/web/app/components/datasets/create/website/watercrawl/index.spec.tsx @@ -1,3 +1,6 @@ +/** + * @vitest-environment jsdom + */ import type { Mock } from 'vitest' import type { CrawlOptions, CrawlResultItem } from '@/models/datasets' import { fireEvent, render, screen, waitFor } from '@testing-library/react' diff --git a/web/app/components/datasets/documents/detail/completed/child-segment-list.spec.tsx b/web/app/components/datasets/documents/detail/completed/child-segment-list.spec.tsx new file mode 100644 index 0000000000..aa3e300322 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/child-segment-list.spec.tsx @@ -0,0 +1,499 @@ +import type { DocumentContextValue } from '@/app/components/datasets/documents/detail/context' +import type { ChildChunkDetail, ChunkingMode, ParentMode } from '@/models/datasets' +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import ChildSegmentList from './child-segment-list' + +// ============================================================================ +// Hoisted Mocks +// ============================================================================ + +const { + mockParentMode, + mockCurrChildChunk, +} = vi.hoisted(() => ({ + mockParentMode: { current: 'paragraph' as ParentMode }, + mockCurrChildChunk: { current: { childChunkInfo: undefined, showModal: false } as { childChunkInfo?: ChildChunkDetail, showModal: boolean } }, +})) + +// Mock react-i18next +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: { count?: number, ns?: string }) => { + if (key === 'segment.childChunks') + return options?.count === 1 ? 'child chunk' : 'child chunks' + if (key === 'segment.searchResults') + return 'search results' + if (key === 'segment.edited') + return 'edited' + if (key === 'operation.add') + return 'Add' + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` + }, + }), +})) + +// Mock document context +vi.mock('../context', () => ({ + useDocumentContext: (selector: (value: DocumentContextValue) => unknown) => { + const value: DocumentContextValue = { + datasetId: 'test-dataset-id', + documentId: 'test-document-id', + docForm: 'text' as ChunkingMode, + parentMode: mockParentMode.current, + } + return selector(value) + }, +})) + +// Mock segment list context +vi.mock('./index', () => ({ + useSegmentListContext: (selector: (value: { currChildChunk: { childChunkInfo?: ChildChunkDetail, showModal: boolean } }) => unknown) => { + return selector({ currChildChunk: mockCurrChildChunk.current }) + }, +})) + +// Mock skeleton component +vi.mock('./skeleton/full-doc-list-skeleton', () => ({ + default: () =>
Loading...
, +})) + +// Mock Empty component +vi.mock('./common/empty', () => ({ + default: ({ onClearFilter }: { onClearFilter: () => void }) => ( +
+ +
+ ), +})) + +// Mock FormattedText and EditSlice +vi.mock('../../../formatted-text/formatted', () => ({ + FormattedText: ({ children, className }: { children: React.ReactNode, className?: string }) => ( +
{children}
+ ), +})) + +vi.mock('../../../formatted-text/flavours/edit-slice', () => ({ + EditSlice: ({ label, text, onDelete, onClick, labelClassName, contentClassName }: { + label: string + text: string + onDelete: () => void + onClick: (e: React.MouseEvent) => void + labelClassName?: string + contentClassName?: string + }) => ( +
+ {label} + {text} + +
+ ), +})) + +// ============================================================================ +// Test Data Factories +// ============================================================================ + +const createMockChildChunk = (overrides: Partial = {}): ChildChunkDetail => ({ + id: `child-${Math.random().toString(36).substr(2, 9)}`, + position: 1, + segment_id: 'segment-1', + content: 'Child chunk content', + word_count: 100, + created_at: 1700000000, + updated_at: 1700000000, + type: 'automatic', + ...overrides, +}) + +// ============================================================================ +// Tests +// ============================================================================ + +describe('ChildSegmentList', () => { + const defaultProps = { + childChunks: [] as ChildChunkDetail[], + parentChunkId: 'parent-1', + enabled: true, + } + + beforeEach(() => { + vi.clearAllMocks() + mockParentMode.current = 'paragraph' + mockCurrChildChunk.current = { childChunkInfo: undefined, showModal: false } + }) + + describe('Rendering', () => { + it('should render with empty child chunks', () => { + render() + + expect(screen.getByText(/child chunks/i)).toBeInTheDocument() + }) + + it('should render child chunks when provided', () => { + const childChunks = [ + createMockChildChunk({ id: 'child-1', position: 1, content: 'First chunk' }), + createMockChildChunk({ id: 'child-2', position: 2, content: 'Second chunk' }), + ] + + render() + + // In paragraph mode, content is collapsed by default + expect(screen.getByText(/2 child chunks/i)).toBeInTheDocument() + }) + + it('should render total count correctly with total prop in full-doc mode', () => { + mockParentMode.current = 'full-doc' + const childChunks = [createMockChildChunk()] + + // Pass inputValue="" to ensure isSearching is false + render() + + expect(screen.getByText(/5 child chunks/i)).toBeInTheDocument() + }) + + it('should render loading skeleton in full-doc mode when loading', () => { + mockParentMode.current = 'full-doc' + + render() + + expect(screen.getByTestId('full-doc-list-skeleton')).toBeInTheDocument() + }) + + it('should not render loading skeleton when not loading', () => { + mockParentMode.current = 'full-doc' + + render() + + expect(screen.queryByTestId('full-doc-list-skeleton')).not.toBeInTheDocument() + }) + }) + + describe('Paragraph Mode', () => { + beforeEach(() => { + mockParentMode.current = 'paragraph' + }) + + it('should show collapse icon in paragraph mode', () => { + const childChunks = [createMockChildChunk()] + + render() + + // Check for collapse/expand behavior + const totalRow = screen.getByText(/1 child chunk/i).closest('div') + expect(totalRow).toBeInTheDocument() + }) + + it('should toggle collapsed state when clicked', () => { + const childChunks = [createMockChildChunk({ content: 'Test content' })] + + render() + + // Initially collapsed in paragraph mode - content should not be visible + expect(screen.queryByTestId('formatted-text')).not.toBeInTheDocument() + + // Find and click the toggle area + const toggleArea = screen.getByText(/1 child chunk/i).closest('div') + + // Click to expand + if (toggleArea) + fireEvent.click(toggleArea) + + // After expansion, content should be visible + expect(screen.getByTestId('formatted-text')).toBeInTheDocument() + }) + + it('should apply opacity when disabled', () => { + const { container } = render() + + const wrapper = container.firstChild + expect(wrapper).toHaveClass('opacity-50') + }) + + it('should not apply opacity when enabled', () => { + const { container } = render() + + const wrapper = container.firstChild + expect(wrapper).not.toHaveClass('opacity-50') + }) + }) + + describe('Full-Doc Mode', () => { + beforeEach(() => { + mockParentMode.current = 'full-doc' + }) + + it('should show content by default in full-doc mode', () => { + const childChunks = [createMockChildChunk({ content: 'Full doc content' })] + + render() + + expect(screen.getByTestId('formatted-text')).toBeInTheDocument() + }) + + it('should render search input in full-doc mode', () => { + render() + + const input = document.querySelector('input') + expect(input).toBeInTheDocument() + }) + + it('should call handleInputChange when input changes', () => { + const handleInputChange = vi.fn() + + render() + + const input = document.querySelector('input') + if (input) { + fireEvent.change(input, { target: { value: 'test search' } }) + expect(handleInputChange).toHaveBeenCalledWith('test search') + } + }) + + it('should show search results text when searching', () => { + render() + + expect(screen.getByText(/3 search results/i)).toBeInTheDocument() + }) + + it('should show empty component when no results and searching', () => { + render( + , + ) + + expect(screen.getByTestId('empty-component')).toBeInTheDocument() + }) + + it('should call onClearFilter when clear button clicked in empty state', () => { + const onClearFilter = vi.fn() + + render( + , + ) + + const clearButton = screen.getByText('Clear Filter') + fireEvent.click(clearButton) + + expect(onClearFilter).toHaveBeenCalled() + }) + }) + + describe('Child Chunk Items', () => { + it('should render edited label when chunk is edited', () => { + mockParentMode.current = 'full-doc' + const editedChunk = createMockChildChunk({ + id: 'edited-chunk', + position: 1, + created_at: 1700000000, + updated_at: 1700000001, // Different from created_at + }) + + render() + + expect(screen.getByText(/C-1 · edited/i)).toBeInTheDocument() + }) + + it('should not show edited label when chunk is not edited', () => { + mockParentMode.current = 'full-doc' + const normalChunk = createMockChildChunk({ + id: 'normal-chunk', + position: 2, + created_at: 1700000000, + updated_at: 1700000000, // Same as created_at + }) + + render() + + expect(screen.getByText('C-2')).toBeInTheDocument() + expect(screen.queryByText(/edited/i)).not.toBeInTheDocument() + }) + + it('should call onClickSlice when chunk is clicked', () => { + mockParentMode.current = 'full-doc' + const onClickSlice = vi.fn() + const chunk = createMockChildChunk({ id: 'clickable-chunk' }) + + render( + , + ) + + const editSlice = screen.getByTestId('edit-slice') + fireEvent.click(editSlice) + + expect(onClickSlice).toHaveBeenCalledWith(chunk) + }) + + it('should call onDelete when delete button is clicked', () => { + mockParentMode.current = 'full-doc' + const onDelete = vi.fn() + const chunk = createMockChildChunk({ id: 'deletable-chunk', segment_id: 'seg-1' }) + + render( + , + ) + + const deleteButton = screen.getByTestId('delete-button') + fireEvent.click(deleteButton) + + expect(onDelete).toHaveBeenCalledWith('seg-1', 'deletable-chunk') + }) + + it('should apply focused styles when chunk is currently selected', () => { + mockParentMode.current = 'full-doc' + const chunk = createMockChildChunk({ id: 'focused-chunk' }) + mockCurrChildChunk.current = { childChunkInfo: chunk, showModal: true } + + render() + + const label = screen.getByTestId('edit-slice-label') + expect(label).toHaveClass('bg-state-accent-solid') + }) + }) + + describe('Add Button', () => { + it('should call handleAddNewChildChunk when Add button is clicked', () => { + const handleAddNewChildChunk = vi.fn() + + render( + , + ) + + const addButton = screen.getByText('Add') + fireEvent.click(addButton) + + expect(handleAddNewChildChunk).toHaveBeenCalledWith('parent-123') + }) + + it('should disable Add button when loading in full-doc mode', () => { + mockParentMode.current = 'full-doc' + + render() + + const addButton = screen.getByText('Add') + expect(addButton).toBeDisabled() + }) + + it('should stop propagation when Add button is clicked', () => { + const handleAddNewChildChunk = vi.fn() + const parentClickHandler = vi.fn() + + render( +
+ +
, + ) + + const addButton = screen.getByText('Add') + fireEvent.click(addButton) + + expect(handleAddNewChildChunk).toHaveBeenCalled() + // Parent should not be called due to stopPropagation + }) + }) + + describe('computeTotalInfo function', () => { + it('should return search results when searching in full-doc mode', () => { + mockParentMode.current = 'full-doc' + + render() + + expect(screen.getByText(/10 search results/i)).toBeInTheDocument() + }) + + it('should return "--" when total is 0 in full-doc mode', () => { + mockParentMode.current = 'full-doc' + + render() + + // When total is 0, displayText is '--' + expect(screen.getByText(/--/)).toBeInTheDocument() + }) + + it('should use childChunks length in paragraph mode', () => { + mockParentMode.current = 'paragraph' + const childChunks = [ + createMockChildChunk(), + createMockChildChunk(), + createMockChildChunk(), + ] + + render() + + expect(screen.getByText(/3 child chunks/i)).toBeInTheDocument() + }) + }) + + describe('Focused State', () => { + it('should not apply opacity when focused even if disabled', () => { + const { container } = render( + , + ) + + const wrapper = container.firstChild + expect(wrapper).not.toHaveClass('opacity-50') + }) + }) + + describe('Input clear button', () => { + it('should call handleInputChange with empty string when clear is clicked', () => { + mockParentMode.current = 'full-doc' + const handleInputChange = vi.fn() + + render( + , + ) + + // Find the clear button (it's the showClearIcon button in Input) + const input = document.querySelector('input') + if (input) { + // Trigger clear by simulating the input's onClear + const clearButton = document.querySelector('[class*="cursor-pointer"]') + if (clearButton) + fireEvent.click(clearButton) + } + }) + }) +}) diff --git a/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx b/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx index b23aac6af9..fd6fd338d0 100644 --- a/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx +++ b/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' import type { ChildChunkDetail } from '@/models/datasets' import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' -import { useMemo, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import Input from '@/app/components/base/input' @@ -29,6 +29,37 @@ type IChildSegmentCardProps = { focused?: boolean } +function computeTotalInfo( + isFullDocMode: boolean, + isSearching: boolean, + total: number | undefined, + childChunksLength: number, +): { displayText: string, count: number, translationKey: 'segment.searchResults' | 'segment.childChunks' } { + if (isSearching) { + const count = total ?? 0 + return { + displayText: count === 0 ? '--' : String(formatNumber(count)), + count, + translationKey: 'segment.searchResults', + } + } + + if (isFullDocMode) { + const count = total ?? 0 + return { + displayText: count === 0 ? '--' : String(formatNumber(count)), + count, + translationKey: 'segment.childChunks', + } + } + + return { + displayText: String(formatNumber(childChunksLength)), + count: childChunksLength, + translationKey: 'segment.childChunks', + } +} + const ChildSegmentList: FC = ({ childChunks, parentChunkId, @@ -49,59 +80,87 @@ const ChildSegmentList: FC = ({ const [collapsed, setCollapsed] = useState(true) - const toggleCollapse = () => { - setCollapsed(!collapsed) + const isParagraphMode = parentMode === 'paragraph' + const isFullDocMode = parentMode === 'full-doc' + const isSearching = inputValue !== '' && isFullDocMode + const contentOpacity = (enabled || focused) ? '' : 'opacity-50 group-hover/card:opacity-100' + const { displayText, count, translationKey } = computeTotalInfo(isFullDocMode, isSearching, total, childChunks.length) + const totalText = `${displayText} ${t(translationKey, { ns: 'datasetDocuments', count })}` + + const toggleCollapse = () => setCollapsed(prev => !prev) + const showContent = (isFullDocMode && !isLoading) || !collapsed + const hoverVisibleClass = isParagraphMode ? 'hidden group-hover/card:inline-block' : '' + + const renderCollapseIcon = () => { + if (!isParagraphMode) + return null + const Icon = collapsed ? RiArrowRightSLine : RiArrowDownSLine + return } - const isParagraphMode = useMemo(() => { - return parentMode === 'paragraph' - }, [parentMode]) + const renderChildChunkItem = (childChunk: ChildChunkDetail) => { + const isEdited = childChunk.updated_at !== childChunk.created_at + const isFocused = currChildChunk?.childChunkInfo?.id === childChunk.id + const label = isEdited + ? `C-${childChunk.position} · ${t('segment.edited', { ns: 'datasetDocuments' })}` + : `C-${childChunk.position}` - const isFullDocMode = useMemo(() => { - return parentMode === 'full-doc' - }, [parentMode]) + return ( + onDelete?.(childChunk.segment_id, childChunk.id)} + className="child-chunk" + labelClassName={isFocused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''} + labelInnerClassName="text-[10px] font-semibold align-bottom leading-6" + contentClassName={cn('!leading-6', isFocused ? 'bg-state-accent-hover-alt text-text-primary' : 'text-text-secondary')} + showDivider={false} + onClick={(e) => { + e.stopPropagation() + onClickSlice?.(childChunk) + }} + offsetOptions={({ rects }) => ({ + mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width, + crossAxis: (20 - rects.floating.height) / 2, + })} + /> + ) + } - const contentOpacity = useMemo(() => { - return (enabled || focused) ? '' : 'opacity-50 group-hover/card:opacity-100' - }, [enabled, focused]) - - const totalText = useMemo(() => { - const isSearch = inputValue !== '' && isFullDocMode - if (!isSearch) { - const text = isFullDocMode - ? !total - ? '--' - : formatNumber(total) - : formatNumber(childChunks.length) - const count = isFullDocMode - ? text === '--' - ? 0 - : total - : childChunks.length - return `${text} ${t('segment.childChunks', { ns: 'datasetDocuments', count })}` + const renderContent = () => { + if (childChunks.length > 0) { + return ( + + {childChunks.map(renderChildChunkItem)} + + ) } - else { - const text = !total ? '--' : formatNumber(total) - const count = text === '--' ? 0 : total - return `${count} ${t('segment.searchResults', { ns: 'datasetDocuments', count })}` + if (inputValue !== '') { + return ( +
+ +
+ ) } - }, [isFullDocMode, total, childChunks.length, inputValue]) + return null + } return (
- {isFullDocMode ? : null} -
+ {isFullDocMode && } +
{ @@ -109,23 +168,15 @@ const ChildSegmentList: FC = ({ toggleCollapse() }} > - { - isParagraphMode - ? collapsed - ? ( - - ) - : () - : null - } + {renderCollapseIcon()} {totalText} - · + ·
- {isFullDocMode - ? ( - handleInputChange?.(e.target.value)} - onClear={() => handleInputChange?.('')} - /> - ) - : null} + {isFullDocMode && ( + handleInputChange?.(e.target.value)} + onClear={() => handleInputChange?.('')} + /> + )}
- {isLoading ? : null} - {((isFullDocMode && !isLoading) || !collapsed) - ? ( -
- {isParagraphMode && ( -
- -
- )} - {childChunks.length > 0 - ? ( - - {childChunks.map((childChunk) => { - const edited = childChunk.updated_at !== childChunk.created_at - const focused = currChildChunk?.childChunkInfo?.id === childChunk.id - return ( - onDelete?.(childChunk.segment_id, childChunk.id)} - className="child-chunk" - labelClassName={focused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''} - labelInnerClassName="text-[10px] font-semibold align-bottom leading-6" - contentClassName={cn('!leading-6', focused ? 'bg-state-accent-hover-alt text-text-primary' : 'text-text-secondary')} - showDivider={false} - onClick={(e) => { - e.stopPropagation() - onClickSlice?.(childChunk) - }} - offsetOptions={({ rects }) => { - return { - mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width, - crossAxis: (20 - rects.floating.height) / 2, - } - }} - /> - ) - })} - - ) - : inputValue !== '' - ? ( -
- -
- ) - : null} + {isLoading && } + {showContent && ( +
+ {isParagraphMode && ( +
+
- ) - : null} + )} + {renderContent()} +
+ )}
) } diff --git a/web/app/components/datasets/documents/detail/completed/common/drawer.tsx b/web/app/components/datasets/documents/detail/completed/common/drawer.tsx index dc1b7192c3..a68742890a 100644 --- a/web/app/components/datasets/documents/detail/completed/common/drawer.tsx +++ b/web/app/components/datasets/documents/detail/completed/common/drawer.tsx @@ -17,6 +17,31 @@ type DrawerProps = { needCheckChunks?: boolean } +const SIDE_POSITION_CLASS = { + right: 'right-0', + left: 'left-0', + bottom: 'bottom-0', + top: 'top-0', +} as const + +function containsTarget(selector: string, target: Node | null): boolean { + const elements = document.querySelectorAll(selector) + return Array.from(elements).some(el => el?.contains(target)) +} + +function shouldReopenChunkDetail( + isClickOnChunk: boolean, + isClickOnChildChunk: boolean, + segmentModalOpen: boolean, + childChunkModalOpen: boolean, +): boolean { + if (segmentModalOpen && isClickOnChildChunk) + return true + if (childChunkModalOpen && isClickOnChunk && !isClickOnChildChunk) + return true + return !isClickOnChunk && !isClickOnChildChunk +} + const Drawer = ({ open, onClose, @@ -41,22 +66,22 @@ const Drawer = ({ const shouldCloseDrawer = useCallback((target: Node | null) => { const panelContent = panelContentRef.current - if (!panelContent) + if (!panelContent || !target) return false - const chunks = document.querySelectorAll('.chunk-card') - const childChunks = document.querySelectorAll('.child-chunk') - const imagePreviewer = document.querySelector('.image-previewer') - const isClickOnChunk = Array.from(chunks).some((chunk) => { - return chunk && chunk.contains(target) - }) - const isClickOnChildChunk = Array.from(childChunks).some((chunk) => { - return chunk && chunk.contains(target) - }) - const reopenChunkDetail = (currSegment.showModal && isClickOnChildChunk) - || (currChildChunk.showModal && isClickOnChunk && !isClickOnChildChunk) || (!isClickOnChunk && !isClickOnChildChunk) - const isClickOnImagePreviewer = imagePreviewer && imagePreviewer.contains(target) - return target && !panelContent.contains(target) && (!needCheckChunks || reopenChunkDetail) && !isClickOnImagePreviewer - }, [currSegment, currChildChunk, needCheckChunks]) + + if (panelContent.contains(target)) + return false + + if (containsTarget('.image-previewer', target)) + return false + + if (!needCheckChunks) + return true + + const isClickOnChunk = containsTarget('.chunk-card', target) + const isClickOnChildChunk = containsTarget('.child-chunk', target) + return shouldReopenChunkDetail(isClickOnChunk, isClickOnChildChunk, currSegment.showModal, currChildChunk.showModal) + }, [currSegment.showModal, currChildChunk.showModal, needCheckChunks]) const onDownCapture = useCallback((e: PointerEvent) => { if (!open || modal) @@ -77,32 +102,27 @@ const Drawer = ({ const isHorizontal = side === 'left' || side === 'right' + const overlayPointerEvents = modal && open ? 'pointer-events-auto' : 'pointer-events-none' + const content = (
- {showOverlay - ? ( -