mirror of
https://github.com/langgenius/dify.git
synced 2026-04-03 23:02:40 +00:00
Compare commits
4 Commits
locate-que
...
codex/dify
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
142f94e27a | ||
|
|
a1bd929b3c | ||
|
|
ffb9ee3e36 | ||
|
|
485586f49a |
@@ -33,7 +33,6 @@ from core.moderation.base import ModerationError
|
||||
from core.moderation.input_moderation import InputModeration
|
||||
from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository
|
||||
from core.workflow.node_factory import get_default_root_node_id
|
||||
from core.workflow.runtime_state import create_graph_runtime_state
|
||||
from core.workflow.system_variables import (
|
||||
build_bootstrap_variables,
|
||||
build_system_variables,
|
||||
@@ -189,11 +188,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
add_node_inputs_to_pool(variable_pool, node_id=root_node_id, inputs=new_inputs)
|
||||
|
||||
# init graph
|
||||
graph_runtime_state = create_graph_runtime_state(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.time(),
|
||||
workflow_id=self._workflow.id,
|
||||
)
|
||||
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.time())
|
||||
graph = self._init_graph(
|
||||
graph_config=self._workflow.graph_dict,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
|
||||
@@ -23,7 +23,6 @@ from core.app.entities.app_invoke_entities import (
|
||||
from core.app.workflow.layers.persistence import PersistenceWorkflowInfo, WorkflowPersistenceLayer
|
||||
from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository
|
||||
from core.workflow.node_factory import DifyNodeFactory, get_default_root_node_id
|
||||
from core.workflow.runtime_state import create_graph_runtime_state
|
||||
from core.workflow.system_variables import build_bootstrap_variables, build_system_variables
|
||||
from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool
|
||||
from core.workflow.workflow_entry import WorkflowEntry
|
||||
@@ -159,11 +158,7 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
workflow.graph_dict
|
||||
)
|
||||
add_node_inputs_to_pool(variable_pool, node_id=root_node_id, inputs=inputs)
|
||||
graph_runtime_state = create_graph_runtime_state(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
workflow_id=workflow.id,
|
||||
)
|
||||
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
|
||||
# init graph
|
||||
graph = self._init_rag_pipeline_graph(
|
||||
|
||||
@@ -16,7 +16,6 @@ from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerat
|
||||
from core.app.workflow.layers.persistence import PersistenceWorkflowInfo, WorkflowPersistenceLayer
|
||||
from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository
|
||||
from core.workflow.node_factory import get_default_root_node_id
|
||||
from core.workflow.runtime_state import create_graph_runtime_state
|
||||
from core.workflow.system_variables import build_bootstrap_variables, build_system_variables
|
||||
from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool
|
||||
from core.workflow.workflow_entry import WorkflowEntry
|
||||
@@ -119,11 +118,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
root_node_id = self._root_node_id or get_default_root_node_id(self._workflow.graph_dict)
|
||||
add_node_inputs_to_pool(variable_pool, node_id=root_node_id, inputs=inputs)
|
||||
|
||||
graph_runtime_state = create_graph_runtime_state(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
workflow_id=self._workflow.id,
|
||||
)
|
||||
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
graph = self._init_graph(
|
||||
graph_config=self._workflow.graph_dict,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
|
||||
@@ -68,7 +68,6 @@ from core.app.entities.queue_entities import (
|
||||
)
|
||||
from core.rag.entities.citation_metadata import RetrievalSourceMetadata
|
||||
from core.workflow.node_factory import DifyNodeFactory, get_default_root_node_id, resolve_workflow_node_class
|
||||
from core.workflow.runtime_state import create_graph_runtime_state
|
||||
from core.workflow.system_variables import (
|
||||
build_bootstrap_variables,
|
||||
default_system_variables,
|
||||
@@ -192,11 +191,7 @@ class WorkflowBasedAppRunner:
|
||||
environment_variables=workflow.environment_variables,
|
||||
),
|
||||
)
|
||||
graph_runtime_state = create_graph_runtime_state(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.time(),
|
||||
workflow_id=workflow.id,
|
||||
)
|
||||
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.time())
|
||||
|
||||
# Determine which type of single node execution and get graph/variable_pool
|
||||
if single_iteration_run:
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
"""Helpers for explicitly wiring GraphRuntimeState collaborators.
|
||||
|
||||
GraphOn currently supports lazy construction of several runtime-state
|
||||
collaborators such as the ready queue, graph execution aggregate, and response
|
||||
coordinator. Dify initializes those collaborators eagerly so repository code
|
||||
does not depend on that implicit behavior.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import AbstractContextManager
|
||||
|
||||
from graphon.graph import Graph
|
||||
from graphon.graph_engine.domain.graph_execution import GraphExecution
|
||||
from graphon.graph_engine.ready_queue import InMemoryReadyQueue
|
||||
from graphon.graph_engine.response_coordinator import ResponseStreamCoordinator
|
||||
from graphon.model_runtime.entities.llm_entities import LLMUsage
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
|
||||
|
||||
def _require_workflow_id(workflow_id: str) -> str:
|
||||
"""Validate that workflow-scoped runtime collaborators receive a real id."""
|
||||
|
||||
if not workflow_id:
|
||||
raise ValueError("workflow_id must be a non-empty string")
|
||||
return workflow_id
|
||||
|
||||
|
||||
def create_graph_runtime_state(
|
||||
*,
|
||||
variable_pool: VariablePool,
|
||||
start_at: float,
|
||||
workflow_id: str,
|
||||
total_tokens: int = 0,
|
||||
llm_usage: LLMUsage | None = None,
|
||||
outputs: dict[str, object] | None = None,
|
||||
node_run_steps: int = 0,
|
||||
execution_context: AbstractContextManager[object] | None = None,
|
||||
) -> GraphRuntimeState:
|
||||
"""Create a runtime state with explicit non-graph collaborators.
|
||||
|
||||
The graph itself is attached later, once node construction has completed and
|
||||
the final Graph instance exists.
|
||||
"""
|
||||
workflow_id = _require_workflow_id(workflow_id)
|
||||
|
||||
return GraphRuntimeState(
|
||||
variable_pool=variable_pool,
|
||||
start_at=start_at,
|
||||
total_tokens=total_tokens,
|
||||
llm_usage=llm_usage or LLMUsage.empty_usage(),
|
||||
outputs=outputs or {},
|
||||
node_run_steps=node_run_steps,
|
||||
ready_queue=InMemoryReadyQueue(),
|
||||
graph_execution=GraphExecution(workflow_id=workflow_id),
|
||||
execution_context=execution_context,
|
||||
)
|
||||
|
||||
|
||||
def ensure_graph_runtime_state_initialized(
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
*,
|
||||
workflow_id: str,
|
||||
) -> GraphRuntimeState:
|
||||
"""Materialize non-graph collaborators when loading legacy or sparse state."""
|
||||
workflow_id = _require_workflow_id(workflow_id)
|
||||
|
||||
if graph_runtime_state._ready_queue is None:
|
||||
graph_runtime_state._ready_queue = InMemoryReadyQueue()
|
||||
|
||||
graph_execution = graph_runtime_state._graph_execution
|
||||
if graph_execution is None:
|
||||
graph_runtime_state._graph_execution = GraphExecution(
|
||||
workflow_id=workflow_id,
|
||||
)
|
||||
elif not graph_execution.workflow_id:
|
||||
graph_execution.workflow_id = workflow_id
|
||||
elif graph_execution.workflow_id != workflow_id:
|
||||
raise ValueError("GraphRuntimeState workflow_id does not match graph execution workflow_id")
|
||||
|
||||
return graph_runtime_state
|
||||
|
||||
|
||||
def bind_graph_runtime_state_to_graph(
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
graph: Graph,
|
||||
*,
|
||||
workflow_id: str,
|
||||
) -> GraphRuntimeState:
|
||||
"""Attach graph-scoped collaborators without relying on GraphOn lazy setup."""
|
||||
|
||||
ensure_graph_runtime_state_initialized(
|
||||
graph_runtime_state,
|
||||
workflow_id=workflow_id,
|
||||
)
|
||||
|
||||
attached_graph = graph_runtime_state._graph
|
||||
if attached_graph is not None and attached_graph is not graph:
|
||||
raise ValueError("GraphRuntimeState already attached to a different graph instance")
|
||||
|
||||
if graph_runtime_state._response_coordinator is None:
|
||||
response_coordinator = ResponseStreamCoordinator(
|
||||
variable_pool=graph_runtime_state.variable_pool,
|
||||
graph=graph,
|
||||
)
|
||||
graph_runtime_state._response_coordinator = response_coordinator
|
||||
|
||||
graph_runtime_state.attach_graph(graph)
|
||||
return graph_runtime_state
|
||||
|
||||
|
||||
def snapshot_graph_runtime_state(
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
*,
|
||||
workflow_id: str,
|
||||
) -> str:
|
||||
"""Serialize runtime state after explicit collaborator initialization."""
|
||||
|
||||
ensure_graph_runtime_state_initialized(
|
||||
graph_runtime_state,
|
||||
workflow_id=workflow_id,
|
||||
)
|
||||
return graph_runtime_state.dumps()
|
||||
@@ -25,10 +25,6 @@ from core.app.file_access import DatabaseFileAccessController
|
||||
from core.app.workflow.layers.llm_quota import LLMQuotaLayer
|
||||
from core.app.workflow.layers.observability import ObservabilityLayer
|
||||
from core.workflow.node_factory import DifyNodeFactory, is_start_node_type, resolve_workflow_node_class
|
||||
from core.workflow.runtime_state import (
|
||||
bind_graph_runtime_state_to_graph,
|
||||
create_graph_runtime_state,
|
||||
)
|
||||
from core.workflow.system_variables import (
|
||||
default_system_variables,
|
||||
get_node_creation_preload_selectors,
|
||||
@@ -76,10 +72,9 @@ class _WorkflowChildEngineBuilder:
|
||||
variable_pool: VariablePool | None = None,
|
||||
) -> GraphEngine:
|
||||
"""Build a child engine with a fresh runtime state and only child-safe layers."""
|
||||
child_graph_runtime_state = create_graph_runtime_state(
|
||||
child_graph_runtime_state = GraphRuntimeState(
|
||||
variable_pool=variable_pool if variable_pool is not None else parent_graph_runtime_state.variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
workflow_id=workflow_id,
|
||||
execution_context=parent_graph_runtime_state.execution_context,
|
||||
)
|
||||
node_factory = DifyNodeFactory(
|
||||
@@ -97,11 +92,6 @@ class _WorkflowChildEngineBuilder:
|
||||
node_factory=node_factory,
|
||||
root_node_id=root_node_id,
|
||||
)
|
||||
bind_graph_runtime_state_to_graph(
|
||||
child_graph_runtime_state,
|
||||
child_graph,
|
||||
workflow_id=workflow_id,
|
||||
)
|
||||
|
||||
command_channel = InMemoryChannel()
|
||||
config = GraphEngineConfig()
|
||||
@@ -162,11 +152,6 @@ class WorkflowEntry:
|
||||
self.command_channel = command_channel
|
||||
execution_context = capture_current_context()
|
||||
graph_runtime_state.execution_context = execution_context
|
||||
bind_graph_runtime_state_to_graph(
|
||||
graph_runtime_state,
|
||||
graph,
|
||||
workflow_id=workflow_id,
|
||||
)
|
||||
self._child_engine_builder = _WorkflowChildEngineBuilder()
|
||||
self.graph_engine = GraphEngine(
|
||||
workflow_id=workflow_id,
|
||||
@@ -259,10 +244,9 @@ class WorkflowEntry:
|
||||
),
|
||||
call_depth=0,
|
||||
)
|
||||
graph_runtime_state = create_graph_runtime_state(
|
||||
graph_runtime_state = GraphRuntimeState(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
workflow_id=workflow.id,
|
||||
execution_context=capture_current_context(),
|
||||
)
|
||||
|
||||
@@ -418,7 +402,7 @@ class WorkflowEntry:
|
||||
),
|
||||
call_depth=0,
|
||||
)
|
||||
graph_runtime_state = create_graph_runtime_state(
|
||||
graph_runtime_state = GraphRuntimeState(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
execution_context=capture_current_context(),
|
||||
|
||||
@@ -8,7 +8,7 @@ from hashlib import sha256
|
||||
from typing import Any, TypedDict, cast
|
||||
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
@@ -1392,10 +1392,10 @@ class RegisterService:
|
||||
db.session.add(dify_setup)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.execute(delete(DifySetup))
|
||||
db.session.execute(delete(TenantAccountJoin))
|
||||
db.session.execute(delete(Account))
|
||||
db.session.execute(delete(Tenant))
|
||||
db.session.query(DifySetup).delete()
|
||||
db.session.query(TenantAccountJoin).delete()
|
||||
db.session.query(Account).delete()
|
||||
db.session.query(Tenant).delete()
|
||||
db.session.commit()
|
||||
|
||||
logger.exception("Setup account failed, email: %s, name: %s", email, name)
|
||||
@@ -1496,11 +1496,7 @@ class RegisterService:
|
||||
TenantService.switch_tenant(account, tenant.id)
|
||||
else:
|
||||
TenantService.check_member_permission(tenant, inviter, account, "add")
|
||||
ta = db.session.scalar(
|
||||
select(TenantAccountJoin)
|
||||
.where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == account.id)
|
||||
.limit(1)
|
||||
)
|
||||
ta = db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id, account_id=account.id).first()
|
||||
|
||||
if not ta:
|
||||
TenantService.create_tenant_member(tenant, account, role)
|
||||
@@ -1557,18 +1553,21 @@ class RegisterService:
|
||||
if not invitation_data:
|
||||
return None
|
||||
|
||||
tenant = db.session.scalar(
|
||||
select(Tenant).where(Tenant.id == invitation_data["workspace_id"], Tenant.status == "normal").limit(1)
|
||||
tenant = (
|
||||
db.session.query(Tenant)
|
||||
.where(Tenant.id == invitation_data["workspace_id"], Tenant.status == "normal")
|
||||
.first()
|
||||
)
|
||||
|
||||
if not tenant:
|
||||
return None
|
||||
|
||||
tenant_account = db.session.execute(
|
||||
select(Account, TenantAccountJoin.role)
|
||||
tenant_account = (
|
||||
db.session.query(Account, TenantAccountJoin.role)
|
||||
.join(TenantAccountJoin, Account.id == TenantAccountJoin.account_id)
|
||||
.where(Account.email == invitation_data["email"], TenantAccountJoin.tenant_id == tenant.id)
|
||||
).first()
|
||||
.first()
|
||||
)
|
||||
|
||||
if not tenant_account:
|
||||
return None
|
||||
|
||||
@@ -4,8 +4,6 @@ import uuid
|
||||
import pandas as pd
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from typing import TypedDict
|
||||
|
||||
from sqlalchemy import or_, select
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from werkzeug.exceptions import NotFound
|
||||
@@ -25,27 +23,6 @@ from tasks.annotation.enable_annotation_reply_task import enable_annotation_repl
|
||||
from tasks.annotation.update_annotation_to_index_task import update_annotation_to_index_task
|
||||
|
||||
|
||||
class AnnotationJobStatusDict(TypedDict):
|
||||
job_id: str
|
||||
job_status: str
|
||||
|
||||
|
||||
class EmbeddingModelDict(TypedDict):
|
||||
embedding_provider_name: str
|
||||
embedding_model_name: str
|
||||
|
||||
|
||||
class AnnotationSettingDict(TypedDict):
|
||||
id: str
|
||||
enabled: bool
|
||||
score_threshold: float
|
||||
embedding_model: EmbeddingModelDict | dict
|
||||
|
||||
|
||||
class AnnotationSettingDisabledDict(TypedDict):
|
||||
enabled: bool
|
||||
|
||||
|
||||
class AppAnnotationService:
|
||||
@classmethod
|
||||
def up_insert_app_annotation_from_message(cls, args: dict, app_id: str) -> MessageAnnotation:
|
||||
@@ -108,7 +85,7 @@ class AppAnnotationService:
|
||||
return annotation
|
||||
|
||||
@classmethod
|
||||
def enable_app_annotation(cls, args: dict, app_id: str) -> AnnotationJobStatusDict:
|
||||
def enable_app_annotation(cls, args: dict, app_id: str):
|
||||
enable_app_annotation_key = f"enable_app_annotation_{str(app_id)}"
|
||||
cache_result = redis_client.get(enable_app_annotation_key)
|
||||
if cache_result is not None:
|
||||
@@ -132,7 +109,7 @@ class AppAnnotationService:
|
||||
return {"job_id": job_id, "job_status": "waiting"}
|
||||
|
||||
@classmethod
|
||||
def disable_app_annotation(cls, app_id: str) -> AnnotationJobStatusDict:
|
||||
def disable_app_annotation(cls, app_id: str):
|
||||
_, current_tenant_id = current_account_with_tenant()
|
||||
disable_app_annotation_key = f"disable_app_annotation_{str(app_id)}"
|
||||
cache_result = redis_client.get(disable_app_annotation_key)
|
||||
@@ -590,7 +567,7 @@ class AppAnnotationService:
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_app_annotation_setting_by_app_id(cls, app_id: str) -> AnnotationSettingDict | AnnotationSettingDisabledDict:
|
||||
def get_app_annotation_setting_by_app_id(cls, app_id: str):
|
||||
_, current_tenant_id = current_account_with_tenant()
|
||||
# get app info
|
||||
app = (
|
||||
@@ -625,9 +602,7 @@ class AppAnnotationService:
|
||||
return {"enabled": False}
|
||||
|
||||
@classmethod
|
||||
def update_app_annotation_setting(
|
||||
cls, app_id: str, annotation_setting_id: str, args: dict
|
||||
) -> AnnotationSettingDict:
|
||||
def update_app_annotation_setting(cls, app_id: str, annotation_setting_id: str, args: dict):
|
||||
current_user, current_tenant_id = current_account_with_tenant()
|
||||
# get app info
|
||||
app = (
|
||||
|
||||
@@ -25,7 +25,7 @@ from graphon.nodes.human_input.entities import HumanInputNodeData, validate_huma
|
||||
from graphon.nodes.human_input.enums import HumanInputFormKind
|
||||
from graphon.nodes.human_input.human_input_node import HumanInputNode
|
||||
from graphon.nodes.start.entities import StartNodeData
|
||||
from graphon.runtime import VariablePool
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.variable_loader import load_into_variable_pool
|
||||
from graphon.variables import VariableBase
|
||||
from graphon.variables.input_entities import VariableEntityType
|
||||
@@ -49,7 +49,6 @@ from core.workflow.human_input_compat import (
|
||||
)
|
||||
from core.workflow.node_factory import LATEST_VERSION, get_node_type_classes_mapping, is_start_node_type
|
||||
from core.workflow.node_runtime import DifyHumanInputNodeRuntime, apply_dify_debug_email_recipient
|
||||
from core.workflow.runtime_state import create_graph_runtime_state
|
||||
from core.workflow.system_variables import build_bootstrap_variables, build_system_variables, default_system_variables
|
||||
from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool
|
||||
from core.workflow.workflow_entry import WorkflowEntry
|
||||
@@ -1147,10 +1146,9 @@ class WorkflowService:
|
||||
),
|
||||
call_depth=0,
|
||||
)
|
||||
graph_runtime_state = create_graph_runtime_state(
|
||||
graph_runtime_state = GraphRuntimeState(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
workflow_id=workflow.id,
|
||||
)
|
||||
node = HumanInputNode(
|
||||
id=node_config["id"],
|
||||
|
||||
@@ -28,7 +28,7 @@ from graphon.graph_engine.entities.commands import GraphEngineCommand
|
||||
from graphon.graph_engine.layers.base import GraphEngineLayerNotInitializedError
|
||||
from graphon.graph_events import GraphRunPausedEvent
|
||||
from graphon.model_runtime.entities.llm_entities import LLMUsage
|
||||
from graphon.runtime import ReadOnlyGraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool
|
||||
from graphon.runtime import GraphRuntimeState, ReadOnlyGraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool
|
||||
from sqlalchemy import Engine, delete, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -38,7 +38,6 @@ from core.app.layers.pause_state_persist_layer import (
|
||||
PauseStatePersistenceLayer,
|
||||
WorkflowResumptionContext,
|
||||
)
|
||||
from core.workflow.runtime_state import create_graph_runtime_state
|
||||
from core.workflow.system_variables import build_system_variables
|
||||
from extensions.ext_storage import storage
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
@@ -204,13 +203,11 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
node_run_steps: int = 0,
|
||||
variables: dict[tuple[str, str], object] | None = None,
|
||||
workflow_run_id: str | None = None,
|
||||
workflow_id: str | None = None,
|
||||
) -> ReadOnlyGraphRuntimeState:
|
||||
"""Create a real GraphRuntimeState for testing."""
|
||||
start_at = time()
|
||||
|
||||
execution_id = workflow_run_id or getattr(self, "test_workflow_run_id", None) or str(uuid.uuid4())
|
||||
resolved_workflow_id = workflow_id or getattr(self, "test_workflow_id", None) or str(uuid.uuid4())
|
||||
|
||||
# Create variable pool
|
||||
variable_pool = VariablePool(system_variables=build_system_variables(workflow_execution_id=execution_id))
|
||||
@@ -222,10 +219,9 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
llm_usage = LLMUsage.empty_usage()
|
||||
|
||||
# Create graph runtime state
|
||||
graph_runtime_state = create_graph_runtime_state(
|
||||
graph_runtime_state = GraphRuntimeState(
|
||||
variable_pool=variable_pool,
|
||||
start_at=start_at,
|
||||
workflow_id=resolved_workflow_id,
|
||||
total_tokens=total_tokens,
|
||||
llm_usage=llm_usage,
|
||||
outputs=outputs or {},
|
||||
|
||||
@@ -26,11 +26,6 @@ from core.repositories.human_input_repository import HumanInputFormEntity, Human
|
||||
from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository
|
||||
from core.repositories.sqlalchemy_workflow_node_execution_repository import SQLAlchemyWorkflowNodeExecutionRepository
|
||||
from core.workflow.node_runtime import DifyHumanInputNodeRuntime
|
||||
from core.workflow.runtime_state import (
|
||||
bind_graph_runtime_state_to_graph,
|
||||
create_graph_runtime_state,
|
||||
snapshot_graph_runtime_state,
|
||||
)
|
||||
from core.workflow.system_variables import build_system_variables
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models import Account
|
||||
@@ -81,11 +76,7 @@ def _build_runtime_state(workflow_execution_id: str, app_id: str, workflow_id: s
|
||||
user_inputs={},
|
||||
conversation_variables=[],
|
||||
)
|
||||
return create_graph_runtime_state(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
workflow_id=workflow_id,
|
||||
)
|
||||
return GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
|
||||
|
||||
def _build_graph(
|
||||
@@ -294,11 +285,6 @@ class TestHumanInputResumeNodeExecutionIntegration:
|
||||
)
|
||||
|
||||
def _run_graph(self, graph: Graph, runtime_state: GraphRuntimeState, execution_id: str) -> None:
|
||||
bind_graph_runtime_state_to_graph(
|
||||
runtime_state,
|
||||
graph,
|
||||
workflow_id=self.workflow.id,
|
||||
)
|
||||
engine = GraphEngine(
|
||||
workflow_id=self.workflow.id,
|
||||
graph=graph,
|
||||
@@ -328,10 +314,7 @@ class TestHumanInputResumeNodeExecutionIntegration:
|
||||
)
|
||||
self._run_graph(paused_graph, runtime_state, execution_id)
|
||||
|
||||
snapshot = snapshot_graph_runtime_state(
|
||||
runtime_state,
|
||||
workflow_id=self.workflow.id,
|
||||
)
|
||||
snapshot = runtime_state.dumps()
|
||||
resumed_state = GraphRuntimeState.from_snapshot(snapshot)
|
||||
resume_repo = _mock_form_repository_with_submission(action_id="continue")
|
||||
resumed_graph = _build_graph(
|
||||
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from graphon.nodes.human_input.entities import HumanInputNodeData
|
||||
from graphon.runtime import VariablePool
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
|
||||
from configs import dify_config
|
||||
from core.app.app_config.entities import WorkflowUIBasedAppConfig
|
||||
@@ -19,7 +19,6 @@ from core.workflow.human_input_compat import (
|
||||
ExternalRecipient,
|
||||
MemberRecipient,
|
||||
)
|
||||
from core.workflow.runtime_state import create_graph_runtime_state, snapshot_graph_runtime_state
|
||||
from extensions.ext_storage import storage
|
||||
from models.account import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom
|
||||
@@ -137,11 +136,7 @@ def _create_workflow_pause_state(
|
||||
)
|
||||
db_session_with_containers.add(workflow_run)
|
||||
|
||||
runtime_state = create_graph_runtime_state(
|
||||
variable_pool=variable_pool,
|
||||
start_at=0.0,
|
||||
workflow_id=workflow_id,
|
||||
)
|
||||
runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=0.0)
|
||||
resumption_context = WorkflowResumptionContext(
|
||||
generate_entity={
|
||||
"type": AppMode.WORKFLOW,
|
||||
@@ -161,10 +156,7 @@ def _create_workflow_pause_state(
|
||||
workflow_execution_id=workflow_run_id,
|
||||
),
|
||||
},
|
||||
serialized_graph_runtime_state=snapshot_graph_runtime_state(
|
||||
runtime_state,
|
||||
workflow_id=workflow_id,
|
||||
),
|
||||
serialized_graph_runtime_state=runtime_state.dumps(),
|
||||
)
|
||||
|
||||
state_object_key = f"workflow_pause_states/{workflow_run_id}.json"
|
||||
|
||||
@@ -30,11 +30,6 @@ from core.app.apps.advanced_chat import app_generator as adv_app_gen_module
|
||||
from core.app.apps.workflow import app_generator as wf_app_gen_module
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.workflow.node_factory import DifyNodeFactory
|
||||
from core.workflow.runtime_state import (
|
||||
bind_graph_runtime_state_to_graph,
|
||||
create_graph_runtime_state,
|
||||
snapshot_graph_runtime_state,
|
||||
)
|
||||
from core.workflow.system_variables import build_system_variables
|
||||
from tests.workflow_test_utils import build_test_graph_init_params
|
||||
|
||||
@@ -173,21 +168,12 @@ def _build_runtime_state(run_id: str) -> GraphRuntimeState:
|
||||
conversation_variables=[],
|
||||
)
|
||||
variable_pool.add(("sys", "workflow_run_id"), run_id)
|
||||
return create_graph_runtime_state(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
workflow_id="workflow",
|
||||
)
|
||||
return GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
|
||||
|
||||
def _run_with_optional_pause(runtime_state: GraphRuntimeState, *, pause_on: str | None) -> list[GraphEngineEvent]:
|
||||
command_channel = InMemoryChannel()
|
||||
graph = _build_graph(runtime_state, pause_on=pause_on)
|
||||
bind_graph_runtime_state_to_graph(
|
||||
runtime_state,
|
||||
graph,
|
||||
workflow_id="workflow",
|
||||
)
|
||||
engine = GraphEngine(
|
||||
workflow_id="workflow",
|
||||
graph=graph,
|
||||
@@ -218,10 +204,7 @@ def test_workflow_app_pause_resume_matches_baseline(mocker):
|
||||
paused_events = _run_with_optional_pause(paused_state, pause_on="tool_a")
|
||||
assert isinstance(paused_events[-1], GraphRunPausedEvent)
|
||||
paused_nodes = _node_successes(paused_events)
|
||||
snapshot = snapshot_graph_runtime_state(
|
||||
paused_state,
|
||||
workflow_id="workflow",
|
||||
)
|
||||
snapshot = paused_state.dumps()
|
||||
|
||||
resumed_state = GraphRuntimeState.from_snapshot(snapshot)
|
||||
|
||||
@@ -261,10 +244,7 @@ def test_advanced_chat_pause_resume_matches_baseline(mocker):
|
||||
paused_events = _run_with_optional_pause(paused_state, pause_on="tool_a")
|
||||
assert isinstance(paused_events[-1], GraphRunPausedEvent)
|
||||
paused_nodes = _node_successes(paused_events)
|
||||
snapshot = snapshot_graph_runtime_state(
|
||||
paused_state,
|
||||
workflow_id="workflow",
|
||||
)
|
||||
snapshot = paused_state.dumps()
|
||||
|
||||
resumed_state = GraphRuntimeState.from_snapshot(snapshot)
|
||||
|
||||
@@ -301,12 +281,7 @@ def test_resume_emits_resumption_start_reason(mocker) -> None:
|
||||
initial_start = next(event for event in paused_events if isinstance(event, GraphRunStartedEvent))
|
||||
assert initial_start.reason == WorkflowStartReason.INITIAL
|
||||
|
||||
resumed_state = GraphRuntimeState.from_snapshot(
|
||||
snapshot_graph_runtime_state(
|
||||
paused_state,
|
||||
workflow_id="workflow",
|
||||
)
|
||||
)
|
||||
resumed_state = GraphRuntimeState.from_snapshot(paused_state.dumps())
|
||||
resumed_events = _run_with_optional_pause(resumed_state, pause_on=None)
|
||||
resume_start = next(event for event in resumed_events if isinstance(event, GraphRunStartedEvent))
|
||||
assert resume_start.reason == WorkflowStartReason.RESUMPTION
|
||||
|
||||
@@ -27,11 +27,10 @@ from graphon.graph_events import (
|
||||
NodeRunSucceededEvent,
|
||||
)
|
||||
from graphon.node_events import NodeRunResult
|
||||
from graphon.runtime import ReadOnlyGraphRuntimeStateWrapper, VariablePool
|
||||
from graphon.runtime import GraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool
|
||||
|
||||
from core.app.entities.app_invoke_entities import WorkflowAppGenerateEntity
|
||||
from core.app.workflow.layers.persistence import PersistenceWorkflowInfo, WorkflowPersistenceLayer
|
||||
from core.workflow.runtime_state import create_graph_runtime_state
|
||||
from core.workflow.system_variables import SystemVariableKey, build_system_variables
|
||||
|
||||
|
||||
@@ -61,11 +60,7 @@ def _make_layer(
|
||||
workflow_execution_id="run-id",
|
||||
conversation_id="conv-id",
|
||||
)
|
||||
runtime_state = create_graph_runtime_state(
|
||||
variable_pool=VariablePool(system_variables=system_variables),
|
||||
start_at=0.0,
|
||||
workflow_id="workflow-id",
|
||||
)
|
||||
runtime_state = GraphRuntimeState(variable_pool=VariablePool(system_variables=system_variables), start_at=0.0)
|
||||
read_only_state = ReadOnlyGraphRuntimeStateWrapper(runtime_state)
|
||||
|
||||
application_generate_entity = WorkflowAppGenerateEntity.model_construct(
|
||||
|
||||
@@ -622,8 +622,7 @@ class MockIterationNode(MockNodeMixin, IterationNode):
|
||||
from graphon.graph import Graph
|
||||
from graphon.graph_engine import GraphEngine, GraphEngineConfig
|
||||
from graphon.graph_engine.command_channels import InMemoryChannel
|
||||
|
||||
from core.workflow.runtime_state import bind_graph_runtime_state_to_graph, create_graph_runtime_state
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
|
||||
# Import our MockNodeFactory instead of DifyNodeFactory
|
||||
from .test_mock_factory import MockNodeFactory
|
||||
@@ -644,10 +643,9 @@ class MockIterationNode(MockNodeMixin, IterationNode):
|
||||
variable_pool_copy.add([self._node_id, "item"], item)
|
||||
|
||||
# Create a new GraphRuntimeState for this iteration
|
||||
graph_runtime_state_copy = create_graph_runtime_state(
|
||||
graph_runtime_state_copy = GraphRuntimeState(
|
||||
variable_pool=variable_pool_copy,
|
||||
start_at=self.graph_runtime_state.start_at,
|
||||
workflow_id=self.workflow_id,
|
||||
total_tokens=0,
|
||||
node_run_steps=0,
|
||||
)
|
||||
@@ -668,11 +666,6 @@ class MockIterationNode(MockNodeMixin, IterationNode):
|
||||
from graphon.nodes.iteration.exc import IterationGraphNotFoundError
|
||||
|
||||
raise IterationGraphNotFoundError("iteration graph not found")
|
||||
bind_graph_runtime_state_to_graph(
|
||||
graph_runtime_state_copy,
|
||||
iteration_graph,
|
||||
workflow_id=self.workflow_id,
|
||||
)
|
||||
|
||||
# Create a new GraphEngine for this iteration
|
||||
graph_engine = GraphEngine(
|
||||
@@ -701,8 +694,7 @@ class MockLoopNode(MockNodeMixin, LoopNode):
|
||||
from graphon.graph import Graph
|
||||
from graphon.graph_engine import GraphEngine, GraphEngineConfig
|
||||
from graphon.graph_engine.command_channels import InMemoryChannel
|
||||
|
||||
from core.workflow.runtime_state import bind_graph_runtime_state_to_graph, create_graph_runtime_state
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
|
||||
# Import our MockNodeFactory instead of DifyNodeFactory
|
||||
from .test_mock_factory import MockNodeFactory
|
||||
@@ -716,10 +708,9 @@ class MockLoopNode(MockNodeMixin, LoopNode):
|
||||
)
|
||||
|
||||
# Create a new GraphRuntimeState for this iteration
|
||||
graph_runtime_state_copy = create_graph_runtime_state(
|
||||
graph_runtime_state_copy = GraphRuntimeState(
|
||||
variable_pool=self.graph_runtime_state.variable_pool,
|
||||
start_at=start_at.timestamp(),
|
||||
workflow_id=self.workflow_id,
|
||||
)
|
||||
|
||||
# Create a MockNodeFactory with the same mock_config
|
||||
@@ -734,11 +725,6 @@ class MockLoopNode(MockNodeMixin, LoopNode):
|
||||
|
||||
if not loop_graph:
|
||||
raise ValueError("loop graph not found")
|
||||
bind_graph_runtime_state_to_graph(
|
||||
graph_runtime_state_copy,
|
||||
loop_graph,
|
||||
workflow_id=self.workflow_id,
|
||||
)
|
||||
|
||||
# Create a new GraphEngine for this iteration
|
||||
graph_engine = GraphEngine(
|
||||
|
||||
@@ -30,11 +30,6 @@ from core.repositories.human_input_repository import (
|
||||
HumanInputFormRepository,
|
||||
)
|
||||
from core.workflow.node_runtime import DifyHumanInputNodeRuntime
|
||||
from core.workflow.runtime_state import (
|
||||
bind_graph_runtime_state_to_graph,
|
||||
create_graph_runtime_state,
|
||||
snapshot_graph_runtime_state,
|
||||
)
|
||||
from core.workflow.system_variables import build_system_variables
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from tests.workflow_test_utils import build_test_graph_init_params
|
||||
@@ -51,10 +46,7 @@ class InMemoryPauseStore:
|
||||
self._snapshot: str | None = None
|
||||
|
||||
def save(self, runtime_state: GraphRuntimeState) -> None:
|
||||
self._snapshot = snapshot_graph_runtime_state(
|
||||
runtime_state,
|
||||
workflow_id="workflow",
|
||||
)
|
||||
self._snapshot = runtime_state.dumps()
|
||||
|
||||
def load(self) -> GraphRuntimeState:
|
||||
assert self._snapshot is not None
|
||||
@@ -130,11 +122,7 @@ def _build_runtime_state() -> GraphRuntimeState:
|
||||
user_inputs={},
|
||||
conversation_variables=[],
|
||||
)
|
||||
return create_graph_runtime_state(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
workflow_id="workflow",
|
||||
)
|
||||
return GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
|
||||
|
||||
def _build_graph(runtime_state: GraphRuntimeState, repo: HumanInputFormRepository) -> Graph:
|
||||
@@ -212,11 +200,6 @@ def _build_graph(runtime_state: GraphRuntimeState, repo: HumanInputFormRepositor
|
||||
|
||||
|
||||
def _run_graph(graph: Graph, runtime_state: GraphRuntimeState) -> list[object]:
|
||||
bind_graph_runtime_state_to_graph(
|
||||
runtime_state,
|
||||
graph,
|
||||
workflow_id="workflow",
|
||||
)
|
||||
engine = GraphEngine(
|
||||
workflow_id="workflow",
|
||||
graph=graph,
|
||||
|
||||
@@ -42,7 +42,6 @@ from graphon.variables import (
|
||||
from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom
|
||||
from core.tools.utils.yaml_utils import _load_yaml_file
|
||||
from core.workflow.node_factory import DifyNodeFactory, get_default_root_node_id
|
||||
from core.workflow.runtime_state import bind_graph_runtime_state_to_graph, create_graph_runtime_state
|
||||
from core.workflow.system_variables import build_bootstrap_variables, build_system_variables
|
||||
from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool
|
||||
|
||||
@@ -66,10 +65,9 @@ class _TableTestChildEngineBuilder:
|
||||
root_node_id: str,
|
||||
variable_pool: VariablePool | None = None,
|
||||
) -> GraphEngine:
|
||||
child_graph_runtime_state = create_graph_runtime_state(
|
||||
child_graph_runtime_state = GraphRuntimeState(
|
||||
variable_pool=variable_pool if variable_pool is not None else parent_graph_runtime_state.variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
workflow_id=workflow_id,
|
||||
execution_context=parent_graph_runtime_state.execution_context,
|
||||
)
|
||||
if self._use_mock_factory:
|
||||
@@ -88,11 +86,6 @@ class _TableTestChildEngineBuilder:
|
||||
child_graph = Graph.init(graph_config=graph_config, node_factory=node_factory, root_node_id=root_node_id)
|
||||
if not child_graph:
|
||||
raise ValueError("child graph not found")
|
||||
bind_graph_runtime_state_to_graph(
|
||||
child_graph_runtime_state,
|
||||
child_graph,
|
||||
workflow_id=workflow_id,
|
||||
)
|
||||
|
||||
child_engine = GraphEngine(
|
||||
workflow_id=workflow_id,
|
||||
@@ -268,11 +261,7 @@ class WorkflowRunner:
|
||||
)
|
||||
add_node_inputs_to_pool(variable_pool, node_id=root_node_id, inputs=root_node_inputs)
|
||||
|
||||
graph_runtime_state = create_graph_runtime_state(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
workflow_id=graph_init_params.workflow_id,
|
||||
)
|
||||
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
|
||||
if use_mock_factory:
|
||||
node_factory = MockNodeFactory(
|
||||
@@ -286,11 +275,6 @@ class WorkflowRunner:
|
||||
node_factory=node_factory,
|
||||
root_node_id=root_node_id,
|
||||
)
|
||||
bind_graph_runtime_state_to_graph(
|
||||
graph_runtime_state,
|
||||
graph,
|
||||
workflow_id=graph_init_params.workflow_id,
|
||||
)
|
||||
|
||||
return graph, graph_runtime_state
|
||||
|
||||
@@ -382,11 +366,6 @@ class TableTestRunner:
|
||||
|
||||
try:
|
||||
graph, graph_runtime_state = self._create_graph_runtime_state(test_case)
|
||||
bind_graph_runtime_state_to_graph(
|
||||
graph_runtime_state,
|
||||
graph,
|
||||
workflow_id="test_workflow",
|
||||
)
|
||||
|
||||
# Create and run the engine with configured worker settings
|
||||
engine = GraphEngine(
|
||||
|
||||
@@ -5,8 +5,6 @@ from graphon.graph_events import (
|
||||
NodeRunStreamChunkEvent,
|
||||
)
|
||||
|
||||
from core.workflow.runtime_state import bind_graph_runtime_state_to_graph
|
||||
|
||||
from .test_table_runner import TableTestRunner
|
||||
|
||||
|
||||
@@ -22,11 +20,6 @@ def test_tool_in_chatflow():
|
||||
query="1",
|
||||
use_mock_factory=True,
|
||||
)
|
||||
bind_graph_runtime_state_to_graph(
|
||||
graph_runtime_state,
|
||||
graph,
|
||||
workflow_id="test_workflow",
|
||||
)
|
||||
|
||||
# Create and run the engine
|
||||
engine = GraphEngine(
|
||||
|
||||
@@ -1034,7 +1034,7 @@ class TestRegisterService:
|
||||
)
|
||||
|
||||
# Verify rollback operations were called
|
||||
mock_db_dependencies["db"].session.execute.assert_called()
|
||||
mock_db_dependencies["db"].session.query.assert_called()
|
||||
|
||||
# ==================== Registration Tests ====================
|
||||
|
||||
@@ -1599,8 +1599,10 @@ class TestRegisterService:
|
||||
mock_session_class.return_value.__exit__.return_value = None
|
||||
mock_lookup.return_value = mock_existing_account
|
||||
|
||||
# Mock scalar for TenantAccountJoin lookup - no existing member
|
||||
mock_db_dependencies["db"].session.scalar.return_value = None
|
||||
# Mock the db.session.query for TenantAccountJoin
|
||||
mock_db_query = MagicMock()
|
||||
mock_db_query.filter_by.return_value.first.return_value = None # No existing member
|
||||
mock_db_dependencies["db"].session.query.return_value = mock_db_query
|
||||
|
||||
# Mock TenantService methods
|
||||
with (
|
||||
@@ -1775,9 +1777,14 @@ class TestRegisterService:
|
||||
}
|
||||
mock_get_invitation_by_token.return_value = invitation_data
|
||||
|
||||
# Mock scalar for tenant lookup, execute for account+role lookup
|
||||
mock_db_dependencies["db"].session.scalar.return_value = mock_tenant
|
||||
mock_db_dependencies["db"].session.execute.return_value.first.return_value = (mock_account, "normal")
|
||||
# Mock database queries - complex query mocking
|
||||
mock_query1 = MagicMock()
|
||||
mock_query1.where.return_value.first.return_value = mock_tenant
|
||||
|
||||
mock_query2 = MagicMock()
|
||||
mock_query2.join.return_value.where.return_value.first.return_value = (mock_account, "normal")
|
||||
|
||||
mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2]
|
||||
|
||||
# Execute test
|
||||
result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123")
|
||||
@@ -1809,8 +1816,10 @@ class TestRegisterService:
|
||||
}
|
||||
mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode()
|
||||
|
||||
# Mock scalar for tenant lookup - not found
|
||||
mock_db_dependencies["db"].session.scalar.return_value = None
|
||||
# Mock database queries - no tenant found
|
||||
mock_query = MagicMock()
|
||||
mock_query.filter.return_value.first.return_value = None
|
||||
mock_db_dependencies["db"].session.query.return_value = mock_query
|
||||
|
||||
# Execute test
|
||||
result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123")
|
||||
@@ -1833,9 +1842,14 @@ class TestRegisterService:
|
||||
}
|
||||
mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode()
|
||||
|
||||
# Mock scalar for tenant, execute for account+role
|
||||
mock_db_dependencies["db"].session.scalar.return_value = mock_tenant
|
||||
mock_db_dependencies["db"].session.execute.return_value.first.return_value = None # No account found
|
||||
# Mock database queries
|
||||
mock_query1 = MagicMock()
|
||||
mock_query1.filter.return_value.first.return_value = mock_tenant
|
||||
|
||||
mock_query2 = MagicMock()
|
||||
mock_query2.join.return_value.where.return_value.first.return_value = None # No account found
|
||||
|
||||
mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2]
|
||||
|
||||
# Execute test
|
||||
result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123")
|
||||
@@ -1861,9 +1875,14 @@ class TestRegisterService:
|
||||
}
|
||||
mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode()
|
||||
|
||||
# Mock scalar for tenant, execute for account+role
|
||||
mock_db_dependencies["db"].session.scalar.return_value = mock_tenant
|
||||
mock_db_dependencies["db"].session.execute.return_value.first.return_value = (mock_account, "normal")
|
||||
# Mock database queries
|
||||
mock_query1 = MagicMock()
|
||||
mock_query1.filter.return_value.first.return_value = mock_tenant
|
||||
|
||||
mock_query2 = MagicMock()
|
||||
mock_query2.join.return_value.where.return_value.first.return_value = (mock_account, "normal")
|
||||
|
||||
mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2]
|
||||
|
||||
# Execute test
|
||||
result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123")
|
||||
|
||||
@@ -9,12 +9,11 @@ from threading import Event
|
||||
import pytest
|
||||
from graphon.entities.pause_reason import HumanInputRequired
|
||||
from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus
|
||||
from graphon.runtime import VariablePool
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
|
||||
from core.app.app_config.entities import WorkflowUIBasedAppConfig
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity
|
||||
from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext, _WorkflowGenerateEntityWrapper
|
||||
from core.workflow.runtime_state import create_graph_runtime_state, snapshot_graph_runtime_state
|
||||
from models.enums import CreatorUserRole
|
||||
from models.model import AppMode
|
||||
from models.workflow import WorkflowRun
|
||||
@@ -117,20 +116,13 @@ def _build_resumption_context(task_id: str) -> WorkflowResumptionContext:
|
||||
call_depth=0,
|
||||
workflow_execution_id="run-1",
|
||||
)
|
||||
runtime_state = create_graph_runtime_state(
|
||||
variable_pool=VariablePool(),
|
||||
start_at=0.0,
|
||||
workflow_id="workflow-1",
|
||||
)
|
||||
runtime_state = GraphRuntimeState(variable_pool=VariablePool(), start_at=0.0)
|
||||
runtime_state.register_paused_node("node-1")
|
||||
runtime_state.outputs = {"result": "value"}
|
||||
wrapper = _WorkflowGenerateEntityWrapper(entity=generate_entity)
|
||||
return WorkflowResumptionContext(
|
||||
generate_entity=wrapper,
|
||||
serialized_graph_runtime_state=snapshot_graph_runtime_state(
|
||||
runtime_state,
|
||||
workflow_id="workflow-1",
|
||||
),
|
||||
serialized_graph_runtime_state=runtime_state.dumps(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"prepare": "vp config"
|
||||
},
|
||||
"devDependencies": {
|
||||
"taze": "catalog:",
|
||||
"vite-plus": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
2
packages/dify-ui/.gitignore
vendored
Normal file
2
packages/dify-ui/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
dist/
|
||||
node_modules/
|
||||
82
packages/dify-ui/package.json
Normal file
82
packages/dify-ui/package.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "@langgenius/dify-ui",
|
||||
"private": true,
|
||||
"version": "0.0.0-private",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"exports": {
|
||||
"./context-menu": {
|
||||
"types": "./dist/context-menu/index.d.ts",
|
||||
"import": "./dist/context-menu/index.js",
|
||||
"default": "./dist/context-menu/index.js"
|
||||
},
|
||||
"./dropdown-menu": {
|
||||
"types": "./dist/dropdown-menu/index.d.ts",
|
||||
"import": "./dist/dropdown-menu/index.js",
|
||||
"default": "./dist/dropdown-menu/index.js"
|
||||
},
|
||||
"./tailwind-preset": {
|
||||
"types": "./dist/tailwind-preset.d.ts",
|
||||
"import": "./dist/tailwind-preset.js",
|
||||
"default": "./dist/tailwind-preset.js"
|
||||
},
|
||||
"./styles.css": "./dist/styles.css",
|
||||
"./markdown.css": "./dist/markdown.css",
|
||||
"./themes/light.css": "./dist/themes/light.css",
|
||||
"./themes/dark.css": "./dist/themes/dark.css",
|
||||
"./themes/manual-light.css": "./dist/themes/manual-light.css",
|
||||
"./themes/manual-dark.css": "./dist/themes/manual-dark.css",
|
||||
"./themes/markdown-light.css": "./dist/themes/markdown-light.css",
|
||||
"./themes/markdown-dark.css": "./dist/themes/markdown-dark.css",
|
||||
"./tokens/tailwind-theme-var-define": {
|
||||
"types": "./dist/tokens/tailwind-theme-var-define.d.ts",
|
||||
"import": "./dist/tokens/tailwind-theme-var-define.js",
|
||||
"default": "./dist/tokens/tailwind-theme-var-define.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ./scripts/build.mjs",
|
||||
"prepack": "pnpm build",
|
||||
"test": "vp test",
|
||||
"test:watch": "vp test --watch",
|
||||
"type-check": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "catalog:",
|
||||
"@dify/iconify-collections": "workspace:*",
|
||||
"@egoist/tailwindcss-icons": "catalog:",
|
||||
"@iconify-json/heroicons": "catalog:",
|
||||
"@iconify-json/ri": "catalog:",
|
||||
"@remixicon/react": "catalog:",
|
||||
"@tailwindcss/typography": "catalog:",
|
||||
"clsx": "catalog:",
|
||||
"tailwind-merge": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/react": "catalog:",
|
||||
"@testing-library/jest-dom": "catalog:",
|
||||
"@testing-library/react": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@vitejs/plugin-react": "catalog:",
|
||||
"happy-dom": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plus": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
31
packages/dify-ui/scripts/build.mjs
Normal file
31
packages/dify-ui/scripts/build.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
import { cp, mkdir, rm } from 'node:fs/promises'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const distDir = resolve(packageRoot, 'dist')
|
||||
|
||||
await rm(distDir, { recursive: true, force: true })
|
||||
|
||||
const tsc = spawnSync('pnpm', ['exec', 'tsc', '-p', 'tsconfig.build.json'], {
|
||||
cwd: packageRoot,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
if (tsc.status !== 0)
|
||||
process.exit(tsc.status ?? 1)
|
||||
|
||||
await mkdir(distDir, { recursive: true })
|
||||
|
||||
await cp(resolve(packageRoot, 'src/styles.css'), resolve(packageRoot, 'dist/styles.css'))
|
||||
await cp(resolve(packageRoot, 'src/markdown.css'), resolve(packageRoot, 'dist/markdown.css'))
|
||||
await cp(resolve(packageRoot, 'src/styles'), resolve(packageRoot, 'dist/styles'), {
|
||||
force: true,
|
||||
recursive: true,
|
||||
})
|
||||
|
||||
await cp(resolve(packageRoot, 'src/themes'), resolve(packageRoot, 'dist/themes'), {
|
||||
force: true,
|
||||
recursive: true,
|
||||
})
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiFileCopyLine,
|
||||
RiPencilLine,
|
||||
RiShareLine,
|
||||
} from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
ContextMenu,
|
||||
@@ -17,7 +23,7 @@ import {
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger,
|
||||
} from '.'
|
||||
} from './index'
|
||||
|
||||
const TriggerArea = ({ label = 'Right-click inside this area' }: { label?: string }) => (
|
||||
<ContextMenuTrigger
|
||||
@@ -185,17 +191,17 @@ export const Complex: Story = {
|
||||
<TriggerArea label="Right-click to inspect all menu capabilities" />
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem>
|
||||
<span aria-hidden className="i-ri-pencil-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiPencilLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem>
|
||||
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiFileCopyLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Duplicate
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger>
|
||||
<span aria-hidden className="i-ri-share-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiShareLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Share
|
||||
</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent>
|
||||
@@ -206,7 +212,7 @@ export const Complex: Story = {
|
||||
</ContextMenuSub>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem destructive>
|
||||
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
|
||||
<RiDeleteBinLine aria-hidden className="size-4 shrink-0" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import type { Placement } from '../internal/placement.js'
|
||||
import { ContextMenu as BaseContextMenu } from '@base-ui/react/context-menu'
|
||||
import { RiArrowRightSLine, RiCheckLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { cn } from '../internal/cn.js'
|
||||
import {
|
||||
menuBackdropClassName,
|
||||
menuGroupLabelClassName,
|
||||
@@ -11,9 +13,8 @@ import {
|
||||
menuPopupBaseClassName,
|
||||
menuRowClassName,
|
||||
menuSeparatorClassName,
|
||||
} from '@/app/components/base/ui/menu-shared'
|
||||
import { parsePlacement } from '@/app/components/base/ui/placement'
|
||||
import { cn } from '@/utils/classnames'
|
||||
} from '../internal/menu-shared.js'
|
||||
import { parsePlacement } from '../internal/placement.js'
|
||||
|
||||
export const ContextMenu = BaseContextMenu.Root
|
||||
export const ContextMenuTrigger = BaseContextMenu.Trigger
|
||||
@@ -42,11 +43,11 @@ type ContextMenuPopupRenderProps = Required<Pick<ContextMenuContentProps, 'child
|
||||
placement: Placement
|
||||
sideOffset: number
|
||||
alignOffset: number
|
||||
className?: string
|
||||
popupClassName?: string
|
||||
positionerProps?: ContextMenuContentProps['positionerProps']
|
||||
popupProps?: ContextMenuContentProps['popupProps']
|
||||
withBackdrop?: boolean
|
||||
className?: string | undefined
|
||||
popupClassName?: string | undefined
|
||||
positionerProps?: ContextMenuContentProps['positionerProps'] | undefined
|
||||
popupProps?: ContextMenuContentProps['popupProps'] | undefined
|
||||
withBackdrop?: boolean | undefined
|
||||
}
|
||||
|
||||
function renderContextMenuPopup({
|
||||
@@ -173,6 +174,25 @@ export function ContextMenuCheckboxItem({
|
||||
)
|
||||
}
|
||||
|
||||
type ContextMenuIndicatorProps = Omit<React.ComponentPropsWithoutRef<'span'>, 'children'> & {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function ContextMenuItemIndicator({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ContextMenuIndicatorProps) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(menuIndicatorClassName, className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <RiCheckLine aria-hidden className="h-4 w-4" />}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
export function ContextMenuCheckboxItemIndicator({
|
||||
className,
|
||||
...props
|
||||
@@ -182,7 +202,7 @@ export function ContextMenuCheckboxItemIndicator({
|
||||
className={cn(menuIndicatorClassName, className)}
|
||||
{...props}
|
||||
>
|
||||
<span aria-hidden className="i-ri-check-line h-4 w-4" />
|
||||
<RiCheckLine aria-hidden className="h-4 w-4" />
|
||||
</BaseContextMenu.CheckboxItemIndicator>
|
||||
)
|
||||
}
|
||||
@@ -196,7 +216,7 @@ export function ContextMenuRadioItemIndicator({
|
||||
className={cn(menuIndicatorClassName, className)}
|
||||
{...props}
|
||||
>
|
||||
<span aria-hidden className="i-ri-check-line h-4 w-4" />
|
||||
<RiCheckLine aria-hidden className="h-4 w-4" />
|
||||
</BaseContextMenu.RadioItemIndicator>
|
||||
)
|
||||
}
|
||||
@@ -217,20 +237,20 @@ export function ContextMenuSubTrigger({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<span aria-hidden className="ml-auto i-ri-arrow-right-s-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiArrowRightSLine aria-hidden className="ml-auto size-4 shrink-0 text-text-tertiary" />
|
||||
</BaseContextMenu.SubmenuTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
type ContextMenuSubContentProps = {
|
||||
children: React.ReactNode
|
||||
placement?: Placement
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
className?: string
|
||||
popupClassName?: string
|
||||
positionerProps?: ContextMenuContentProps['positionerProps']
|
||||
popupProps?: ContextMenuContentProps['popupProps']
|
||||
placement?: Placement | undefined
|
||||
sideOffset?: number | undefined
|
||||
alignOffset?: number | undefined
|
||||
className?: string | undefined
|
||||
popupClassName?: string | undefined
|
||||
positionerProps?: ContextMenuContentProps['positionerProps'] | undefined
|
||||
popupProps?: ContextMenuContentProps['popupProps'] | undefined
|
||||
}
|
||||
|
||||
export function ContextMenuSubContent({
|
||||
@@ -278,3 +298,5 @@ export function ContextMenuSeparator({
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export type { Placement }
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react'
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Link from '@/next/link'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -14,20 +13,20 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '../index'
|
||||
|
||||
vi.mock('@/next/link', () => ({
|
||||
default: ({
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
href: string
|
||||
children?: ReactNode
|
||||
} & Omit<ComponentPropsWithoutRef<'a'>, 'href'>) => (
|
||||
function MockLink({
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
href: string
|
||||
children?: ReactNode
|
||||
} & Omit<ComponentPropsWithoutRef<'a'>, 'href'>) {
|
||||
return (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
describe('dropdown-menu wrapper', () => {
|
||||
describe('DropdownMenuContent', () => {
|
||||
@@ -301,7 +300,7 @@ describe('dropdown-menu wrapper', () => {
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLinkItem
|
||||
render={<Link href="/account" />}
|
||||
render={<MockLink href="/account" />}
|
||||
aria-label="account link"
|
||||
>
|
||||
Account settings
|
||||
@@ -1,4 +1,15 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import {
|
||||
RiArchiveLine,
|
||||
RiChat1Line,
|
||||
RiDeleteBinLine,
|
||||
RiFileCopyLine,
|
||||
RiLink,
|
||||
RiLockLine,
|
||||
RiMailLine,
|
||||
RiPencilLine,
|
||||
RiShareLine,
|
||||
} from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -17,7 +28,7 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '.'
|
||||
} from './index'
|
||||
|
||||
const TriggerButton = ({ label = 'Open Menu' }: { label?: string }) => (
|
||||
<DropdownMenuTrigger
|
||||
@@ -214,20 +225,20 @@ export const WithIcons: Story = {
|
||||
<TriggerButton />
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-pencil-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiPencilLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiFileCopyLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-archive-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiArchiveLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Archive
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem destructive>
|
||||
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
|
||||
<RiDeleteBinLine aria-hidden className="size-4 shrink-0" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -262,35 +273,35 @@ const ComplexDemo = () => {
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuGroupLabel>Edit</DropdownMenuGroupLabel>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-pencil-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiPencilLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiFileCopyLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled>
|
||||
<span aria-hidden className="i-ri-lock-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiLockLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Move to Workspace
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<span aria-hidden className="i-ri-share-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiShareLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Share
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-mail-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiMailLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Email
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-chat-1-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiChat1Line aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Slack
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<span aria-hidden className="i-ri-link size-4 shrink-0 text-text-tertiary" />
|
||||
<RiLink aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Copy Link
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
@@ -315,13 +326,13 @@ const ComplexDemo = () => {
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuCheckboxItem checked={showArchived} onCheckedChange={setShowArchived}>
|
||||
<span aria-hidden className="i-ri-archive-line size-4 shrink-0 text-text-tertiary" />
|
||||
<RiArchiveLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
|
||||
Show Archived
|
||||
<DropdownMenuCheckboxItemIndicator />
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem destructive>
|
||||
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
|
||||
<RiDeleteBinLine aria-hidden className="size-4 shrink-0" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import type { Placement } from '../internal/placement.js'
|
||||
import { Menu } from '@base-ui/react/menu'
|
||||
import { RiArrowRightSLine, RiCheckLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { cn } from '../internal/cn.js'
|
||||
import {
|
||||
menuGroupLabelClassName,
|
||||
menuIndicatorClassName,
|
||||
@@ -10,9 +12,8 @@ import {
|
||||
menuPopupBaseClassName,
|
||||
menuRowClassName,
|
||||
menuSeparatorClassName,
|
||||
} from '@/app/components/base/ui/menu-shared'
|
||||
import { parsePlacement } from '@/app/components/base/ui/placement'
|
||||
import { cn } from '@/utils/classnames'
|
||||
} from '../internal/menu-shared.js'
|
||||
import { parsePlacement } from '../internal/placement.js'
|
||||
|
||||
export const DropdownMenu = Menu.Root
|
||||
export const DropdownMenuTrigger = Menu.Trigger
|
||||
@@ -41,7 +42,7 @@ export function DropdownMenuRadioItemIndicator({
|
||||
className={cn(menuIndicatorClassName, className)}
|
||||
{...props}
|
||||
>
|
||||
<span aria-hidden className="i-ri-check-line h-4 w-4" />
|
||||
<RiCheckLine aria-hidden className="h-4 w-4" />
|
||||
</Menu.RadioItemIndicator>
|
||||
)
|
||||
}
|
||||
@@ -67,7 +68,7 @@ export function DropdownMenuCheckboxItemIndicator({
|
||||
className={cn(menuIndicatorClassName, className)}
|
||||
{...props}
|
||||
>
|
||||
<span aria-hidden className="i-ri-check-line h-4 w-4" />
|
||||
<RiCheckLine aria-hidden className="h-4 w-4" />
|
||||
</Menu.CheckboxItemIndicator>
|
||||
)
|
||||
}
|
||||
@@ -105,10 +106,10 @@ type DropdownMenuPopupRenderProps = Required<Pick<DropdownMenuContentProps, 'chi
|
||||
placement: Placement
|
||||
sideOffset: number
|
||||
alignOffset: number
|
||||
className?: string
|
||||
popupClassName?: string
|
||||
positionerProps?: DropdownMenuContentProps['positionerProps']
|
||||
popupProps?: DropdownMenuContentProps['popupProps']
|
||||
className?: string | undefined
|
||||
popupClassName?: string | undefined
|
||||
positionerProps?: DropdownMenuContentProps['positionerProps'] | undefined
|
||||
popupProps?: DropdownMenuContentProps['popupProps'] | undefined
|
||||
}
|
||||
|
||||
function renderDropdownMenuPopup({
|
||||
@@ -186,20 +187,20 @@ export function DropdownMenuSubTrigger({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<span aria-hidden className="i-ri-arrow-right-s-line ml-auto size-4 shrink-0 text-text-tertiary" />
|
||||
<RiArrowRightSLine aria-hidden className="ml-auto size-4 shrink-0 text-text-tertiary" />
|
||||
</Menu.SubmenuTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
type DropdownMenuSubContentProps = {
|
||||
children: React.ReactNode
|
||||
placement?: Placement
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
className?: string
|
||||
popupClassName?: string
|
||||
positionerProps?: DropdownMenuContentProps['positionerProps']
|
||||
popupProps?: DropdownMenuContentProps['popupProps']
|
||||
placement?: Placement | undefined
|
||||
sideOffset?: number | undefined
|
||||
alignOffset?: number | undefined
|
||||
className?: string | undefined
|
||||
popupClassName?: string | undefined
|
||||
positionerProps?: DropdownMenuContentProps['positionerProps'] | undefined
|
||||
popupProps?: DropdownMenuContentProps['popupProps'] | undefined
|
||||
}
|
||||
|
||||
export function DropdownMenuSubContent({
|
||||
@@ -271,3 +272,5 @@ export function DropdownMenuSeparator({
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export type { Placement }
|
||||
7
packages/dify-ui/src/internal/cn.ts
Normal file
7
packages/dify-ui/src/internal/cn.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ClassValue } from 'clsx'
|
||||
import { clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
25
packages/dify-ui/src/internal/placement.ts
Normal file
25
packages/dify-ui/src/internal/placement.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
type Side = 'top' | 'bottom' | 'left' | 'right'
|
||||
type Align = 'start' | 'center' | 'end'
|
||||
|
||||
export type Placement
|
||||
= 'top'
|
||||
| 'top-start'
|
||||
| 'top-end'
|
||||
| 'right'
|
||||
| 'right-start'
|
||||
| 'right-end'
|
||||
| 'bottom'
|
||||
| 'bottom-start'
|
||||
| 'bottom-end'
|
||||
| 'left'
|
||||
| 'left-start'
|
||||
| 'left-end'
|
||||
|
||||
export function parsePlacement(placement: Placement): { side: Side, align: Align } {
|
||||
const [side, align] = placement.split('-') as [Side, Align | undefined]
|
||||
|
||||
return {
|
||||
side,
|
||||
align: align ?? 'center',
|
||||
}
|
||||
}
|
||||
2
packages/dify-ui/src/markdown.css
Normal file
2
packages/dify-ui/src/markdown.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import './themes/markdown-light.css';
|
||||
@import './themes/markdown-dark.css';
|
||||
7
packages/dify-ui/src/styles.css
Normal file
7
packages/dify-ui/src/styles.css
Normal file
@@ -0,0 +1,7 @@
|
||||
@import './themes/light.css' layer(base);
|
||||
@import './themes/dark.css' layer(base);
|
||||
@import './themes/manual-light.css' layer(base);
|
||||
@import './themes/manual-dark.css' layer(base);
|
||||
@import './styles/tokens.css';
|
||||
|
||||
@source './**/*.{js,mjs}';
|
||||
713
packages/dify-ui/src/styles/tokens.css
Normal file
713
packages/dify-ui/src/styles/tokens.css
Normal file
@@ -0,0 +1,713 @@
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
|
||||
@utility system-kbd {
|
||||
/* font define start */
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-regular-uppercase {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-regular {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-medium {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-medium-uppercase {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-semibold-uppercase {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-regular {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-regular-uppercase {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-medium {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-medium-uppercase {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-semibold {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-semibold-uppercase {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-regular {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-medium {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-medium-uppercase {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-semibold {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-semibold-uppercase {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-md-regular {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-md-medium {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-md-semibold {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-md-semibold-uppercase {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xl-regular {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xl-medium {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xl-semibold {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-xs-regular {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-xs-semibold {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-sm-regular {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-sm-semibold {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-md-regular {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-md-semibold {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xs-light {
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xs-regular {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xs-medium {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-sm-light {
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-sm-regular {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-sm-medium {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-md-light {
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-md-regular {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-md-medium {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-lg-light {
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-lg-regular {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-lg-medium {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xl-regular {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xl-medium {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xl-light {
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-2xl-light {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-2xl-regular {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-2xl-medium {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-xs-semi-bold {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-xs-bold {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-sm-semi-bold {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-sm-bold {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-md-semi-bold {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-md-bold {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-lg-semi-bold {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-lg-bold {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-xl-semi-bold {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-xl-bold {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-2xl-semi-bold {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-2xl-bold {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-3xl-semi-bold {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-3xl-bold {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-4xl-semi-bold {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-4xl-bold {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-5xl-semi-bold {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-5xl-bold {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-6xl-semi-bold {
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-6xl-bold {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-7xl-semi-bold {
|
||||
font-size: 48px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-7xl-bold {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-8xl-semi-bold {
|
||||
font-size: 60px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-8xl-bold {
|
||||
font-size: 60px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-2xs {
|
||||
/* font define end */
|
||||
|
||||
/* border radius start */
|
||||
border-radius: 2px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-xs {
|
||||
border-radius: 4px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-sm {
|
||||
border-radius: 6px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-md {
|
||||
border-radius: 8px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-lg {
|
||||
border-radius: 10px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-xl {
|
||||
border-radius: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-2xl {
|
||||
border-radius: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-3xl {
|
||||
border-radius: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-4xl {
|
||||
border-radius: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-5xl {
|
||||
border-radius: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-6xl {
|
||||
border-radius: 28px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-7xl {
|
||||
border-radius: 32px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-8xl {
|
||||
border-radius: 40px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-9xl {
|
||||
border-radius: 48px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-full {
|
||||
border-radius: 64px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility no-scrollbar {
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
@utility no-spinner {
|
||||
/* Hide arrows from number input */
|
||||
&::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { icons as customPublicIcons } from '@dify/iconify-collections/custom-public'
|
||||
import { icons as customVenderIcons } from '@dify/iconify-collections/custom-vender'
|
||||
import { getIconCollections, iconsPlugin } from '@egoist/tailwindcss-icons'
|
||||
import { icons as heroicons } from '@iconify-json/heroicons'
|
||||
import { icons as remixIcons } from '@iconify-json/ri'
|
||||
import { iconsPlugin } from '@egoist/tailwindcss-icons'
|
||||
import tailwindTypography from '@tailwindcss/typography'
|
||||
import tailwindThemeVarDefine from './themes/tailwind-theme-var-define'
|
||||
import tailwindThemeVarDefine from './tokens/tailwind-theme-var-define.js'
|
||||
import typography from './typography.js'
|
||||
|
||||
const config = {
|
||||
@@ -151,7 +153,8 @@ const config = {
|
||||
tailwindTypography,
|
||||
iconsPlugin({
|
||||
collections: {
|
||||
...getIconCollections(['heroicons', 'ri']),
|
||||
heroicons,
|
||||
ri: remixIcons,
|
||||
'custom-public': customPublicIcons,
|
||||
'custom-vender': customVenderIcons,
|
||||
},
|
||||
3
packages/dify-ui/src/typography.d.ts
vendored
Normal file
3
packages/dify-ui/src/typography.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare const typography: (helpers: { theme: (path: string) => unknown }) => Record<string, unknown>
|
||||
|
||||
export default typography
|
||||
8
packages/dify-ui/tailwind.config.ts
Normal file
8
packages/dify-ui/tailwind.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import difyUiTailwindPreset from './src/tailwind-preset'
|
||||
|
||||
const config = {
|
||||
content: [],
|
||||
...difyUiTailwindPreset,
|
||||
}
|
||||
|
||||
export default config
|
||||
21
packages/dify-ui/tsconfig.build.json
Normal file
21
packages/dify-ui/tsconfig.build.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.js"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.stories.tsx",
|
||||
"src/**/__tests__/**"
|
||||
]
|
||||
}
|
||||
38
packages/dify-ui/tsconfig.json
Normal file
38
packages/dify-ui/tsconfig.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"jsx": "react-jsx",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"es2022"
|
||||
],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node",
|
||||
"vitest/globals",
|
||||
"@testing-library/jest-dom"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.js",
|
||||
"scripts/**/*.mjs",
|
||||
"vite.config.ts",
|
||||
"vitest.setup.ts"
|
||||
]
|
||||
}
|
||||
11
packages/dify-ui/vite.config.ts
Normal file
11
packages/dify-ui/vite.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite-plus'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
globals: true,
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
},
|
||||
})
|
||||
39
packages/dify-ui/vitest.setup.ts
Normal file
39
packages/dify-ui/vitest.setup.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { cleanup } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { afterEach } from 'vitest'
|
||||
|
||||
if (typeof globalThis.ResizeObserver === 'undefined') {
|
||||
globalThis.ResizeObserver = class {
|
||||
observe() {
|
||||
return undefined
|
||||
}
|
||||
|
||||
unobserve() {
|
||||
return undefined
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof globalThis.IntersectionObserver === 'undefined') {
|
||||
globalThis.IntersectionObserver = class {
|
||||
readonly root: Element | Document | null = null
|
||||
readonly rootMargin = ''
|
||||
readonly scrollMargin = ''
|
||||
readonly thresholds: ReadonlyArray<number> = []
|
||||
constructor(_callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) {}
|
||||
observe(_target: Element) {}
|
||||
unobserve(_target: Element) {}
|
||||
disconnect() {}
|
||||
takeRecords(): IntersectionObserverEntry[] {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
639
pnpm-lock.yaml
generated
639
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -41,7 +41,6 @@ overrides:
|
||||
is-generator-function: npm:@nolyfill/is-generator-function@^1.0.44
|
||||
is-typed-array: npm:@nolyfill/is-typed-array@^1.0.44
|
||||
isarray: npm:@nolyfill/isarray@^1.0.44
|
||||
lodash@>=4.0.0 <= 4.17.23: 4.18.0
|
||||
lodash-es@>=4.0.0 <= 4.17.23: 4.18.0
|
||||
object.assign: npm:@nolyfill/object.assign@^1.0.44
|
||||
object.entries: npm:@nolyfill/object.entries@^1.0.44
|
||||
@@ -132,7 +131,6 @@ catalog:
|
||||
"@tanstack/react-form-devtools": 0.2.20
|
||||
"@tanstack/react-query": 5.96.1
|
||||
"@tanstack/react-query-devtools": 5.96.1
|
||||
"@tanstack/react-virtual": 3.13.23
|
||||
"@testing-library/dom": 10.4.1
|
||||
"@testing-library/jest-dom": 6.9.1
|
||||
"@testing-library/react": 16.3.2
|
||||
@@ -148,6 +146,8 @@ catalog:
|
||||
"@types/qs": 6.15.0
|
||||
"@types/react": 19.2.14
|
||||
"@types/react-dom": 19.2.3
|
||||
"@types/react-syntax-highlighter": 15.5.13
|
||||
"@types/react-window": 1.8.8
|
||||
"@types/sortablejs": 1.15.9
|
||||
"@typescript-eslint/eslint-plugin": 8.58.0
|
||||
"@typescript-eslint/parser": 8.58.0
|
||||
@@ -162,7 +162,7 @@ catalog:
|
||||
class-variance-authority: 0.7.1
|
||||
clsx: 2.1.1
|
||||
cmdk: 1.1.1
|
||||
code-inspector-plugin: 1.5.1
|
||||
code-inspector-plugin: 1.5.0
|
||||
copy-to-clipboard: 3.3.3
|
||||
cron-parser: 5.5.0
|
||||
dayjs: 1.11.20
|
||||
@@ -187,7 +187,6 @@ catalog:
|
||||
fast-deep-equal: 3.1.3
|
||||
foxact: 0.3.0
|
||||
happy-dom: 20.8.9
|
||||
hast-util-to-jsx-runtime: 2.3.6
|
||||
hono: 4.12.10
|
||||
html-entities: 2.6.0
|
||||
html-to-image: 1.11.13
|
||||
@@ -228,13 +227,14 @@ catalog:
|
||||
react-pdf-highlighter: 8.0.0-rc.0
|
||||
react-server-dom-webpack: 19.2.4
|
||||
react-sortablejs: 6.1.4
|
||||
react-syntax-highlighter: 15.6.6
|
||||
react-textarea-autosize: 8.5.9
|
||||
react-window: 1.8.11
|
||||
reactflow: 11.11.4
|
||||
remark-breaks: 4.0.0
|
||||
remark-directive: 4.0.0
|
||||
scheduler: 0.27.0
|
||||
sharp: 0.34.5
|
||||
shiki: 4.0.2
|
||||
sortablejs: 1.15.7
|
||||
std-semver: 1.0.8
|
||||
storybook: 10.3.4
|
||||
@@ -242,6 +242,7 @@ catalog:
|
||||
string-ts: 2.3.1
|
||||
tailwind-merge: 3.5.0
|
||||
tailwindcss: 4.2.2
|
||||
taze: 19.11.0
|
||||
tldts: 7.0.27
|
||||
tsdown: 0.21.7
|
||||
tsx: 4.21.0
|
||||
|
||||
10
taze.config.js
Normal file
10
taze.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'taze'
|
||||
|
||||
export default defineConfig({
|
||||
exclude: [
|
||||
// We are going to replace these
|
||||
'react-syntax-highlighter',
|
||||
'react-window',
|
||||
'@types/react-window',
|
||||
],
|
||||
})
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { StorybookConfig } from '@storybook/nextjs-vite'
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
stories: [
|
||||
'../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||
'../../packages/dify-ui/src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||
],
|
||||
addons: [
|
||||
// Not working with Storybook Vite framework
|
||||
// '@storybook/addon-onboarding',
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
const mockVirtualizer = ({
|
||||
count,
|
||||
estimateSize,
|
||||
}: {
|
||||
count: number
|
||||
estimateSize?: (index: number) => number
|
||||
}) => {
|
||||
const getSize = (index: number) => estimateSize?.(index) ?? 0
|
||||
|
||||
return {
|
||||
getTotalSize: () => Array.from({ length: count }).reduce<number>((total, _, index) => total + getSize(index), 0),
|
||||
getVirtualItems: () => {
|
||||
let start = 0
|
||||
|
||||
return Array.from({ length: count }).map((_, index) => {
|
||||
const size = getSize(index)
|
||||
const virtualItem = {
|
||||
end: start + size,
|
||||
index,
|
||||
key: index,
|
||||
size,
|
||||
start,
|
||||
}
|
||||
|
||||
start += size
|
||||
return virtualItem
|
||||
})
|
||||
},
|
||||
measureElement: vi.fn(),
|
||||
scrollToIndex: vi.fn(),
|
||||
}
|
||||
}
|
||||
|
||||
export { mockVirtualizer as useVirtualizer }
|
||||
@@ -5,10 +5,6 @@ import { Theme } from '@/types/app'
|
||||
|
||||
import CodeBlock from '../code-block'
|
||||
|
||||
const { mockHighlightCode } = vi.hoisted(() => ({
|
||||
mockHighlightCode: vi.fn(),
|
||||
}))
|
||||
|
||||
type UseThemeReturn = {
|
||||
theme: Theme
|
||||
}
|
||||
@@ -74,10 +70,6 @@ vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => mockUseTheme(),
|
||||
}))
|
||||
|
||||
vi.mock('../shiki-highlight', () => ({
|
||||
highlightCode: mockHighlightCode,
|
||||
}))
|
||||
|
||||
vi.mock('echarts', () => ({
|
||||
getInstanceByDom: mockEcharts.getInstanceByDom,
|
||||
}))
|
||||
@@ -138,11 +130,6 @@ describe('CodeBlock', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseTheme.mockReturnValue({ theme: Theme.light })
|
||||
mockHighlightCode.mockImplementation(async ({ code, language }) => (
|
||||
<pre className="shiki">
|
||||
<code className={`language-${language}`}>{code}</code>
|
||||
</pre>
|
||||
))
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
clientWidthSpy = vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(900)
|
||||
@@ -211,13 +198,11 @@ describe('CodeBlock', () => {
|
||||
expect(container.querySelector('code')?.textContent).toBe('plain text')
|
||||
})
|
||||
|
||||
it('should render syntax-highlighted output when language is standard', async () => {
|
||||
it('should render syntax-highlighted output when language is standard', () => {
|
||||
render(<CodeBlock className="language-javascript">const x = 1;</CodeBlock>)
|
||||
|
||||
expect(screen.getByText('JavaScript')).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('code.language-javascript')?.textContent).toContain('const x = 1;')
|
||||
})
|
||||
expect(document.querySelector('code.language-javascript')?.textContent).toContain('const x = 1;')
|
||||
})
|
||||
|
||||
it('should format unknown language labels with capitalized fallback when language is not in map', () => {
|
||||
@@ -257,26 +242,13 @@ describe('CodeBlock', () => {
|
||||
expect(screen.queryByText(/Error rendering SVG/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render syntax-highlighted output when language is standard and app theme is dark', async () => {
|
||||
it('should render syntax-highlighted output when language is standard and app theme is dark', () => {
|
||||
mockUseTheme.mockReturnValue({ theme: Theme.dark })
|
||||
|
||||
render(<CodeBlock className="language-javascript">const y = 2;</CodeBlock>)
|
||||
|
||||
expect(screen.getByText('JavaScript')).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('code.language-javascript')?.textContent).toContain('const y = 2;')
|
||||
})
|
||||
})
|
||||
|
||||
it('should fall back to plain code block when shiki highlighting fails', async () => {
|
||||
mockHighlightCode.mockRejectedValueOnce(new Error('highlight failed'))
|
||||
|
||||
render(<CodeBlock className="language-javascript">const z = 3;</CodeBlock>)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('const z = 3;')).toBeInTheDocument()
|
||||
})
|
||||
expect(document.querySelector('code.language-javascript')).toBeNull()
|
||||
expect(document.querySelector('code.language-javascript')?.textContent).toContain('const y = 2;')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { JSX } from 'react'
|
||||
import type { BundledLanguage, BundledTheme } from 'shiki/bundle/web'
|
||||
import ReactEcharts from 'echarts-for-react'
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter'
|
||||
import {
|
||||
atelierHeathDark,
|
||||
atelierHeathLight,
|
||||
} from 'react-syntax-highlighter/dist/esm/styles/hljs'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import CopyIcon from '@/app/components/base/copy-icon'
|
||||
import MarkdownMusic from '@/app/components/base/markdown-blocks/music'
|
||||
@@ -11,10 +14,10 @@ import useTheme from '@/hooks/use-theme'
|
||||
import dynamic from '@/next/dynamic'
|
||||
import { Theme } from '@/types/app'
|
||||
import SVGRenderer from '../svg-gallery' // Assumes svg-gallery.tsx is in /base directory
|
||||
import { highlightCode } from './shiki-highlight'
|
||||
|
||||
const Flowchart = dynamic(() => import('@/app/components/base/mermaid'), { ssr: false })
|
||||
|
||||
// Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
|
||||
const capitalizationLanguageNameMap: Record<string, string> = {
|
||||
sql: 'SQL',
|
||||
javascript: 'JavaScript',
|
||||
@@ -61,61 +64,6 @@ const getCorrectCapitalizationLanguageName = (language: string) => {
|
||||
// visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message
|
||||
// or use the non-minified dev environment for full errors and additional helpful warnings.
|
||||
|
||||
const ShikiCodeBlock = memo(({ code, language, theme, initial }: { code: string, language: string, theme: BundledTheme, initial?: JSX.Element }) => {
|
||||
const [nodes, setNodes] = useState(initial)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
void highlightCode({
|
||||
code,
|
||||
language: language as BundledLanguage,
|
||||
theme,
|
||||
}).then((result) => {
|
||||
if (!cancelled)
|
||||
setNodes(result)
|
||||
}).catch((error) => {
|
||||
console.error('Shiki highlighting failed:', error)
|
||||
if (!cancelled)
|
||||
setNodes(undefined)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [code, language, theme])
|
||||
|
||||
if (!nodes) {
|
||||
return (
|
||||
<pre style={{
|
||||
paddingLeft: 12,
|
||||
borderBottomLeftRadius: '10px',
|
||||
borderBottomRightRadius: '10px',
|
||||
backgroundColor: 'var(--color-components-input-bg-normal)',
|
||||
margin: 0,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderBottomLeftRadius: '10px',
|
||||
borderBottomRightRadius: '10px',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
className="shiki-line-numbers [&_pre]:m-0! [&_pre]:rounded-t-none! [&_pre]:rounded-b-[10px]! [&_pre]:bg-components-input-bg-normal! [&_pre]:py-2!"
|
||||
>
|
||||
{nodes}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
ShikiCodeBlock.displayName = 'ShikiCodeBlock'
|
||||
|
||||
// Define ECharts event parameter types
|
||||
type EChartsEventParams = {
|
||||
type: string
|
||||
@@ -468,11 +416,20 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<ShikiCodeBlock
|
||||
code={content}
|
||||
language={match?.[1] || 'text'}
|
||||
theme={isDarkMode ? 'github-dark' : 'github-light'}
|
||||
/>
|
||||
<SyntaxHighlighter
|
||||
{...props}
|
||||
style={theme === Theme.light ? atelierHeathLight : atelierHeathDark}
|
||||
customStyle={{
|
||||
paddingLeft: 12,
|
||||
borderBottomLeftRadius: '10px',
|
||||
borderBottomRightRadius: '10px',
|
||||
backgroundColor: 'var(--color-components-input-bg-normal)',
|
||||
}}
|
||||
language={match?.[1]}
|
||||
showLineNumbers
|
||||
>
|
||||
{content}
|
||||
</SyntaxHighlighter>
|
||||
)
|
||||
}
|
||||
}, [children, language, isSVG, finalChartOption, props, theme, match, chartState, isDarkMode, echartsStyle, echartsOpts, handleChartReady, echartsEvents])
|
||||
@@ -483,7 +440,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="flex h-8 items-center justify-between rounded-t-[10px] border-b border-divider-subtle bg-components-input-bg-normal p-1 pl-3">
|
||||
<div className="system-xs-semibold-uppercase text-text-secondary">{languageShowName}</div>
|
||||
<div className="text-text-secondary system-xs-semibold-uppercase">{languageShowName}</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{language === 'svg' && <SVGBtn isSVG={isSVG} setIsSVG={setIsSVG} />}
|
||||
<ActionButton>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { JSX } from 'react'
|
||||
import type { BundledLanguage, BundledTheme } from 'shiki/bundle/web'
|
||||
import { toJsxRuntime } from 'hast-util-to-jsx-runtime'
|
||||
import { Fragment } from 'react'
|
||||
import { jsx, jsxs } from 'react/jsx-runtime'
|
||||
import { codeToHast } from 'shiki/bundle/web'
|
||||
|
||||
type HighlightCodeOptions = {
|
||||
code: string
|
||||
language: BundledLanguage
|
||||
theme: BundledTheme
|
||||
}
|
||||
|
||||
export const highlightCode = async ({
|
||||
code,
|
||||
language,
|
||||
theme,
|
||||
}: HighlightCodeOptions): Promise<JSX.Element> => {
|
||||
const hast = await codeToHast(code, {
|
||||
lang: language,
|
||||
theme,
|
||||
})
|
||||
|
||||
return toJsxRuntime(hast, {
|
||||
Fragment,
|
||||
jsx,
|
||||
jsxs,
|
||||
}) as JSX.Element
|
||||
}
|
||||
@@ -9,8 +9,6 @@ import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useInvalidPreImportNotionPages, usePreImportNotionPages } from '@/service/knowledge/use-import'
|
||||
import NotionPageSelector from '../base'
|
||||
|
||||
vi.mock('@tanstack/react-virtual')
|
||||
|
||||
vi.mock('@/service/knowledge/use-import', () => ({
|
||||
usePreImportNotionPages: vi.fn(),
|
||||
useInvalidPreImportNotionPages: vi.fn(),
|
||||
@@ -185,7 +183,7 @@ describe('NotionPageSelector Base', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<NotionPageSelector credentialList={mockCredentialList} onSelect={vi.fn()} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.dataSource.notion.selector.configure' }))
|
||||
await user.click(screen.getByRole('button', { name: 'Configure Notion' }))
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.DATA_SOURCE })
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { DataSourceCredential } from '../../header/account-setting/data-sou
|
||||
import type { NotionCredential } from './credential-selector'
|
||||
import type { DataSourceNotionPageMap, DataSourceNotionWorkspace, NotionPage } from '@/models/common'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useInvalidPreImportNotionPages, usePreImportNotionPages } from '@/service/knowledge/use-import'
|
||||
@@ -34,7 +33,6 @@ const NotionPageSelector = ({
|
||||
credentialList,
|
||||
onSelectCredential,
|
||||
}: NotionPageSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
||||
|
||||
@@ -50,34 +48,27 @@ const NotionPageSelector = ({
|
||||
}
|
||||
})
|
||||
}, [credentialList])
|
||||
const [selectedCredentialId, setSelectedCredentialId] = useState(() => notionCredentials[0]?.credentialId ?? '')
|
||||
const currentCredential = useMemo(() => {
|
||||
return notionCredentials.find(item => item.credentialId === selectedCredentialId) ?? notionCredentials[0] ?? null
|
||||
}, [notionCredentials, selectedCredentialId])
|
||||
const currentCredentialId = currentCredential?.credentialId ?? ''
|
||||
const [currentCredential, setCurrentCredential] = useState(notionCredentials[0])
|
||||
|
||||
useEffect(() => {
|
||||
onSelectCredential?.(currentCredentialId)
|
||||
}, [currentCredentialId, onSelectCredential])
|
||||
|
||||
useEffect(() => {
|
||||
if (!notionCredentials.length) {
|
||||
onSelect([])
|
||||
return
|
||||
const credential = notionCredentials.find(item => item.credentialId === currentCredential?.credentialId)
|
||||
if (!credential) {
|
||||
const firstCredential = notionCredentials[0]
|
||||
invalidPreImportNotionPages({ datasetId, credentialId: firstCredential.credentialId })
|
||||
setCurrentCredential(notionCredentials[0])
|
||||
onSelect([]) // Clear selected pages when changing credential
|
||||
onSelectCredential?.(firstCredential.credentialId)
|
||||
}
|
||||
|
||||
if (!selectedCredentialId || selectedCredentialId === currentCredentialId)
|
||||
return
|
||||
|
||||
invalidPreImportNotionPages({ datasetId, credentialId: currentCredentialId })
|
||||
onSelect([])
|
||||
}, [currentCredentialId, datasetId, invalidPreImportNotionPages, notionCredentials.length, onSelect, selectedCredentialId])
|
||||
else {
|
||||
onSelectCredential?.(credential?.credentialId || '')
|
||||
}
|
||||
}, [notionCredentials])
|
||||
|
||||
const {
|
||||
data: notionsPages,
|
||||
isFetching: isFetchingNotionPages,
|
||||
isError: isFetchingNotionPagesError,
|
||||
} = usePreImportNotionPages({ datasetId, credentialId: currentCredentialId })
|
||||
} = usePreImportNotionPages({ datasetId, credentialId: currentCredential.credentialId || '' })
|
||||
|
||||
const pagesMapAndSelectedPagesId: [DataSourceNotionPageMap, Set<string>, Set<string>] = useMemo(() => {
|
||||
const selectedPagesId = new Set<string>()
|
||||
@@ -103,24 +94,28 @@ const NotionPageSelector = ({
|
||||
const defaultSelectedPagesId = useMemo(() => {
|
||||
return [...Array.from(pagesMapAndSelectedPagesId[1]), ...(value || [])]
|
||||
}, [pagesMapAndSelectedPagesId, value])
|
||||
const selectedPagesId = useMemo(() => new Set(defaultSelectedPagesId), [defaultSelectedPagesId])
|
||||
const [selectedPagesId, setSelectedPagesId] = useState<Set<string>>(() => new Set(defaultSelectedPagesId))
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedPagesId(new Set(defaultSelectedPagesId))
|
||||
}, [defaultSelectedPagesId])
|
||||
|
||||
const handleSearchValueChange = useCallback((value: string) => {
|
||||
setSearchValue(value)
|
||||
}, [])
|
||||
|
||||
const handleSelectCredential = useCallback((credentialId: string) => {
|
||||
if (credentialId === currentCredentialId)
|
||||
return
|
||||
|
||||
invalidPreImportNotionPages({ datasetId, credentialId })
|
||||
setSelectedCredentialId(credentialId)
|
||||
const credential = notionCredentials.find(item => item.credentialId === credentialId)!
|
||||
invalidPreImportNotionPages({ datasetId, credentialId: credential.credentialId })
|
||||
setCurrentCredential(credential)
|
||||
onSelect([]) // Clear selected pages when changing credential
|
||||
}, [currentCredentialId, datasetId, invalidPreImportNotionPages, onSelect])
|
||||
onSelectCredential?.(credential.credentialId)
|
||||
}, [datasetId, invalidPreImportNotionPages, notionCredentials, onSelect, onSelectCredential])
|
||||
|
||||
const handleSelectPages = useCallback((newSelectedPagesId: Set<string>) => {
|
||||
const selectedPages = Array.from(newSelectedPagesId).map(pageId => pagesMapAndSelectedPagesId[0][pageId])
|
||||
|
||||
setSelectedPagesId(new Set(Array.from(newSelectedPagesId)))
|
||||
onSelect(selectedPages)
|
||||
}, [pagesMapAndSelectedPagesId, onSelect])
|
||||
|
||||
@@ -145,16 +140,16 @@ const NotionPageSelector = ({
|
||||
<div className="flex flex-col gap-y-2" data-testid="notion-page-selector-base">
|
||||
<Header
|
||||
onClickConfiguration={handleConfigureNotion}
|
||||
title={t('dataSource.notion.selector.headerTitle', { ns: 'common' })}
|
||||
buttonText={t('dataSource.notion.selector.configure', { ns: 'common' })}
|
||||
docTitle={t('dataSource.notion.selector.docs', { ns: 'common' })}
|
||||
title="Choose notion pages"
|
||||
buttonText="Configure Notion"
|
||||
docTitle="Notion docs"
|
||||
docLink="https://www.notion.so/docs"
|
||||
/>
|
||||
<div className="rounded-xl border border-components-panel-border bg-background-default-subtle">
|
||||
<div className="flex h-12 items-center gap-x-2 rounded-t-xl border-b border-b-divider-regular bg-components-panel-bg p-2">
|
||||
<div className="flex grow items-center gap-x-1">
|
||||
<WorkspaceSelector
|
||||
value={currentCredentialId}
|
||||
value={currentCredential.credentialId}
|
||||
items={notionCredentials}
|
||||
onSelect={handleSelectCredential}
|
||||
/>
|
||||
@@ -173,7 +168,6 @@ const NotionPageSelector = ({
|
||||
)
|
||||
: (
|
||||
<PageSelector
|
||||
key={currentCredentialId || 'default'}
|
||||
value={selectedPagesId}
|
||||
disabledValue={pagesMapAndSelectedPagesId[2]}
|
||||
searchValue={searchValue}
|
||||
|
||||
@@ -3,8 +3,6 @@ import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import PageSelector from '../index'
|
||||
|
||||
vi.mock('@tanstack/react-virtual')
|
||||
|
||||
const buildPage = (overrides: Partial<DataSourceNotionPage>): DataSourceNotionPage => ({
|
||||
page_id: 'page-id',
|
||||
page_name: 'Page name',
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { ListChildComponentProps } from 'react-window'
|
||||
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
||||
import { memo, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePageSelectorModel } from './use-page-selector-model'
|
||||
import VirtualPageList from './virtual-page-list'
|
||||
import { areEqual, FixedSizeList as List } from 'react-window'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Checkbox from '../../checkbox'
|
||||
import NotionIcon from '../../notion-icon'
|
||||
|
||||
type PageSelectorProps = {
|
||||
value: Set<string>
|
||||
@@ -13,7 +17,173 @@ type PageSelectorProps = {
|
||||
canPreview?: boolean
|
||||
previewPageId?: string
|
||||
onPreview?: (selectedPageId: string) => void
|
||||
isMultipleChoice?: boolean
|
||||
}
|
||||
type NotionPageTreeItem = {
|
||||
children: Set<string>
|
||||
descendants: Set<string>
|
||||
depth: number
|
||||
ancestors: string[]
|
||||
} & DataSourceNotionPage
|
||||
type NotionPageTreeMap = Record<string, NotionPageTreeItem>
|
||||
type NotionPageItem = {
|
||||
expand: boolean
|
||||
depth: number
|
||||
} & DataSourceNotionPage
|
||||
|
||||
const recursivePushInParentDescendants = (
|
||||
pagesMap: DataSourceNotionPageMap,
|
||||
listTreeMap: NotionPageTreeMap,
|
||||
current: NotionPageTreeItem,
|
||||
leafItem: NotionPageTreeItem,
|
||||
) => {
|
||||
const parentId = current.parent_id
|
||||
const pageId = current.page_id
|
||||
|
||||
if (!parentId || !pageId)
|
||||
return
|
||||
|
||||
if (parentId !== 'root' && pagesMap[parentId]) {
|
||||
if (!listTreeMap[parentId]) {
|
||||
const children = new Set([pageId])
|
||||
const descendants = new Set([pageId, leafItem.page_id])
|
||||
listTreeMap[parentId] = {
|
||||
...pagesMap[parentId],
|
||||
children,
|
||||
descendants,
|
||||
depth: 0,
|
||||
ancestors: [],
|
||||
}
|
||||
}
|
||||
else {
|
||||
listTreeMap[parentId].children.add(pageId)
|
||||
listTreeMap[parentId].descendants.add(pageId)
|
||||
listTreeMap[parentId].descendants.add(leafItem.page_id)
|
||||
}
|
||||
leafItem.depth++
|
||||
leafItem.ancestors.unshift(listTreeMap[parentId].page_name)
|
||||
|
||||
if (listTreeMap[parentId].parent_id !== 'root')
|
||||
recursivePushInParentDescendants(pagesMap, listTreeMap, listTreeMap[parentId], leafItem)
|
||||
}
|
||||
}
|
||||
|
||||
const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
|
||||
dataList: NotionPageItem[]
|
||||
handleToggle: (index: number) => void
|
||||
checkedIds: Set<string>
|
||||
disabledCheckedIds: Set<string>
|
||||
handleCheck: (index: number) => void
|
||||
canPreview?: boolean
|
||||
handlePreview: (index: number) => void
|
||||
listMapWithChildrenAndDescendants: NotionPageTreeMap
|
||||
searchValue: string
|
||||
previewPageId: string
|
||||
pagesMap: DataSourceNotionPageMap
|
||||
}>) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
dataList,
|
||||
handleToggle,
|
||||
checkedIds,
|
||||
disabledCheckedIds,
|
||||
handleCheck,
|
||||
canPreview,
|
||||
handlePreview,
|
||||
listMapWithChildrenAndDescendants,
|
||||
searchValue,
|
||||
previewPageId,
|
||||
pagesMap,
|
||||
} = data
|
||||
const current = dataList[index]
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id]
|
||||
const hasChild = currentWithChildrenAndDescendants.descendants.size > 0
|
||||
const ancestors = currentWithChildrenAndDescendants.ancestors
|
||||
const breadCrumbs = ancestors.length ? [...ancestors, current.page_name] : [current.page_name]
|
||||
const disabled = disabledCheckedIds.has(current.page_id)
|
||||
|
||||
const renderArrow = () => {
|
||||
if (hasChild) {
|
||||
return (
|
||||
<div
|
||||
className="mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover"
|
||||
style={{ marginLeft: current.depth * 8 }}
|
||||
onClick={() => handleToggle(index)}
|
||||
data-testid={`notion-page-toggle-${current.page_id}`}
|
||||
>
|
||||
{
|
||||
current.expand
|
||||
? <div className="i-ri-arrow-down-s-line h-4 w-4 text-text-tertiary" />
|
||||
: <div className="i-ri-arrow-right-s-line h-4 w-4 text-text-tertiary" />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (current.parent_id === 'root' || !pagesMap[current.parent_id]) {
|
||||
return (
|
||||
<div></div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="mr-1 h-5 w-5 shrink-0" style={{ marginLeft: current.depth * 8 }} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group flex cursor-pointer items-center rounded-md pl-2 pr-[2px] hover:bg-state-base-hover', previewPageId === current.page_id && 'bg-state-base-hover')}
|
||||
style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }}
|
||||
data-testid={`notion-page-row-${current.page_id}`}
|
||||
>
|
||||
<Checkbox
|
||||
className="mr-2 shrink-0"
|
||||
checked={checkedIds.has(current.page_id)}
|
||||
disabled={disabled}
|
||||
onCheck={() => {
|
||||
handleCheck(index)
|
||||
}}
|
||||
id={`notion-page-checkbox-${current.page_id}`}
|
||||
/>
|
||||
{!searchValue && renderArrow()}
|
||||
<NotionIcon
|
||||
className="mr-1 shrink-0"
|
||||
type="page"
|
||||
src={current.page_icon}
|
||||
/>
|
||||
<div
|
||||
className="grow truncate text-[13px] font-medium leading-4 text-text-secondary"
|
||||
title={current.page_name}
|
||||
data-testid={`notion-page-name-${current.page_id}`}
|
||||
>
|
||||
{current.page_name}
|
||||
</div>
|
||||
{
|
||||
canPreview && (
|
||||
<div
|
||||
className="ml-1 hidden h-6 shrink-0 cursor-pointer items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-xs
|
||||
font-medium leading-4 text-components-button-secondary-text shadow-xs shadow-shadow-shadow-3 backdrop-blur-[10px]
|
||||
hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover group-hover:flex"
|
||||
onClick={() => handlePreview(index)}
|
||||
data-testid={`notion-page-preview-${current.page_id}`}
|
||||
>
|
||||
{t('dataSource.notion.selector.preview', { ns: 'common' })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
searchValue && (
|
||||
<div
|
||||
className="ml-1 max-w-[120px] shrink-0 truncate text-xs text-text-quaternary"
|
||||
title={breadCrumbs.join(' / ')}
|
||||
>
|
||||
{breadCrumbs.join(' / ')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const Item = memo(ItemComponent, areEqual)
|
||||
|
||||
const PageSelector = ({
|
||||
value,
|
||||
@@ -27,25 +197,108 @@ const PageSelector = ({
|
||||
onPreview,
|
||||
}: PageSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
currentPreviewPageId,
|
||||
effectiveSearchValue,
|
||||
rows,
|
||||
handlePreview,
|
||||
handleSelect,
|
||||
handleToggle,
|
||||
} = usePageSelectorModel({
|
||||
checkedIds: value,
|
||||
list,
|
||||
onPreview,
|
||||
onSelect,
|
||||
pagesMap,
|
||||
previewPageId,
|
||||
searchValue,
|
||||
selectionMode: 'multiple',
|
||||
})
|
||||
const [dataList, setDataList] = useState<NotionPageItem[]>([])
|
||||
const [localPreviewPageId, setLocalPreviewPageId] = useState('')
|
||||
|
||||
if (!rows.length) {
|
||||
useEffect(() => {
|
||||
setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
expand: false,
|
||||
depth: 0,
|
||||
}
|
||||
}))
|
||||
}, [list])
|
||||
|
||||
const searchDataList = list.filter((item) => {
|
||||
return item.page_name.includes(searchValue)
|
||||
}).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
expand: false,
|
||||
depth: 0,
|
||||
}
|
||||
})
|
||||
const currentDataList = searchValue ? searchDataList : dataList
|
||||
const currentPreviewPageId = previewPageId === undefined ? localPreviewPageId : previewPageId
|
||||
|
||||
const listMapWithChildrenAndDescendants = useMemo(() => {
|
||||
return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
|
||||
const pageId = next.page_id
|
||||
if (!prev[pageId])
|
||||
prev[pageId] = { ...next, children: new Set(), descendants: new Set(), depth: 0, ancestors: [] }
|
||||
|
||||
recursivePushInParentDescendants(pagesMap, prev, prev[pageId], prev[pageId])
|
||||
return prev
|
||||
}, {})
|
||||
}, [list, pagesMap])
|
||||
|
||||
const handleToggle = (index: number) => {
|
||||
const current = dataList[index]
|
||||
const pageId = current.page_id
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
||||
const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants)
|
||||
const childrenIds = Array.from(currentWithChildrenAndDescendants.children)
|
||||
let newDataList = []
|
||||
|
||||
if (current.expand) {
|
||||
current.expand = false
|
||||
|
||||
newDataList = dataList.filter(item => !descendantsIds.includes(item.page_id))
|
||||
}
|
||||
else {
|
||||
current.expand = true
|
||||
|
||||
newDataList = [
|
||||
...dataList.slice(0, index + 1),
|
||||
...childrenIds.map(item => ({
|
||||
...pagesMap[item],
|
||||
expand: false,
|
||||
depth: listMapWithChildrenAndDescendants[item].depth,
|
||||
})),
|
||||
...dataList.slice(index + 1),
|
||||
]
|
||||
}
|
||||
setDataList(newDataList)
|
||||
}
|
||||
|
||||
const copyValue = new Set(value)
|
||||
const handleCheck = (index: number) => {
|
||||
const current = currentDataList[index]
|
||||
const pageId = current.page_id
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
||||
|
||||
if (copyValue.has(pageId)) {
|
||||
if (!searchValue) {
|
||||
for (const item of currentWithChildrenAndDescendants.descendants)
|
||||
copyValue.delete(item)
|
||||
}
|
||||
|
||||
copyValue.delete(pageId)
|
||||
}
|
||||
else {
|
||||
if (!searchValue) {
|
||||
for (const item of currentWithChildrenAndDescendants.descendants)
|
||||
copyValue.add(item)
|
||||
}
|
||||
|
||||
copyValue.add(pageId)
|
||||
}
|
||||
|
||||
onSelect(new Set(copyValue))
|
||||
}
|
||||
|
||||
const handlePreview = (index: number) => {
|
||||
const current = currentDataList[index]
|
||||
const pageId = current.page_id
|
||||
|
||||
setLocalPreviewPageId(pageId)
|
||||
|
||||
if (onPreview)
|
||||
onPreview(pageId)
|
||||
}
|
||||
|
||||
if (!currentDataList.length) {
|
||||
return (
|
||||
<div className="flex h-[296px] items-center justify-center text-[13px] text-text-tertiary">
|
||||
{t('dataSource.notion.selector.noSearchResult', { ns: 'common' })}
|
||||
@@ -54,18 +307,29 @@ const PageSelector = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualPageList
|
||||
checkedIds={value}
|
||||
disabledValue={disabledValue}
|
||||
onPreview={handlePreview}
|
||||
onSelect={handleSelect}
|
||||
onToggle={handleToggle}
|
||||
previewPageId={currentPreviewPageId}
|
||||
rows={rows}
|
||||
searchValue={effectiveSearchValue}
|
||||
selectionMode="multiple"
|
||||
showPreview={canPreview}
|
||||
/>
|
||||
<List
|
||||
className="py-2"
|
||||
height={296}
|
||||
itemCount={currentDataList.length}
|
||||
itemSize={28}
|
||||
width="100%"
|
||||
itemKey={(index, data) => data.dataList[index].page_id}
|
||||
itemData={{
|
||||
dataList: currentDataList,
|
||||
handleToggle,
|
||||
checkedIds: value,
|
||||
disabledCheckedIds: disabledValue,
|
||||
handleCheck,
|
||||
canPreview,
|
||||
handlePreview,
|
||||
listMapWithChildrenAndDescendants,
|
||||
searchValue,
|
||||
previewPageId: currentPreviewPageId,
|
||||
pagesMap,
|
||||
}}
|
||||
>
|
||||
{Item}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
import type { NotionPageRow as NotionPageRowData, NotionPageSelectionMode } from './types'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import NotionIcon from '@/app/components/base/notion-icon'
|
||||
import Radio from '@/app/components/base/radio/ui'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type NotionPageRowProps = {
|
||||
checked: boolean
|
||||
disabled: boolean
|
||||
isPreviewed: boolean
|
||||
onPreview: (pageId: string) => void
|
||||
onSelect: (pageId: string) => void
|
||||
onToggle: (pageId: string) => void
|
||||
row: NotionPageRowData
|
||||
searchValue: string
|
||||
selectionMode: NotionPageSelectionMode
|
||||
showPreview: boolean
|
||||
style: CSSProperties
|
||||
}
|
||||
|
||||
const NotionPageRow = ({
|
||||
checked,
|
||||
disabled,
|
||||
isPreviewed,
|
||||
onPreview,
|
||||
onSelect,
|
||||
onToggle,
|
||||
row,
|
||||
searchValue,
|
||||
selectionMode,
|
||||
showPreview,
|
||||
style,
|
||||
}: NotionPageRowProps) => {
|
||||
const { t } = useTranslation()
|
||||
const pageId = row.page.page_id
|
||||
const breadcrumbs = row.ancestors.length ? [...row.ancestors, row.page.page_name] : [row.page.page_name]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group flex cursor-pointer items-center rounded-md pr-[2px] pl-2 hover:bg-state-base-hover', isPreviewed && 'bg-state-base-hover')}
|
||||
style={style}
|
||||
data-testid={`notion-page-row-${pageId}`}
|
||||
>
|
||||
{selectionMode === 'multiple'
|
||||
? (
|
||||
<Checkbox
|
||||
className="mr-2 shrink-0"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onCheck={() => onSelect(pageId)}
|
||||
id={`notion-page-checkbox-${pageId}`}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Radio
|
||||
className="mr-2 shrink-0"
|
||||
isChecked={checked}
|
||||
disabled={disabled}
|
||||
onCheck={() => onSelect(pageId)}
|
||||
/>
|
||||
)}
|
||||
{!searchValue && row.hasChild && (
|
||||
<div
|
||||
className="mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover"
|
||||
style={{ marginLeft: row.depth * 8 }}
|
||||
onClick={() => onToggle(pageId)}
|
||||
data-testid={`notion-page-toggle-${pageId}`}
|
||||
>
|
||||
{row.expand
|
||||
? <RiArrowDownSLine className="h-4 w-4 text-text-tertiary" />
|
||||
: <RiArrowRightSLine className="h-4 w-4 text-text-tertiary" />}
|
||||
</div>
|
||||
)}
|
||||
{!searchValue && !row.hasChild && row.parentExists && (
|
||||
<div className="mr-1 h-5 w-5 shrink-0" style={{ marginLeft: row.depth * 8 }} />
|
||||
)}
|
||||
<NotionIcon
|
||||
className="mr-1 shrink-0"
|
||||
type="page"
|
||||
src={row.page.page_icon}
|
||||
/>
|
||||
<div
|
||||
className="grow truncate text-[13px] leading-4 font-medium text-text-secondary"
|
||||
title={row.page.page_name}
|
||||
data-testid={`notion-page-name-${pageId}`}
|
||||
>
|
||||
{row.page.page_name}
|
||||
</div>
|
||||
{showPreview && (
|
||||
<div
|
||||
className="ml-1 hidden h-6 shrink-0 cursor-pointer items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-xs
|
||||
leading-4 font-medium text-components-button-secondary-text shadow-xs shadow-shadow-shadow-3 backdrop-blur-[10px]
|
||||
group-hover:flex hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover"
|
||||
onClick={() => onPreview(pageId)}
|
||||
data-testid={`notion-page-preview-${pageId}`}
|
||||
>
|
||||
{t('dataSource.notion.selector.preview', { ns: 'common' })}
|
||||
</div>
|
||||
)}
|
||||
{searchValue && (
|
||||
<div
|
||||
className="ml-1 max-w-[120px] shrink-0 truncate text-xs text-text-quaternary"
|
||||
title={breadcrumbs.join(' / ')}
|
||||
>
|
||||
{breadcrumbs.join(' / ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NotionPageRow)
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { DataSourceNotionPage } from '@/models/common'
|
||||
|
||||
export type NotionPageSelectionMode = 'multiple' | 'single'
|
||||
|
||||
export type NotionPageTreeItem = {
|
||||
children: Set<string>
|
||||
descendants: Set<string>
|
||||
depth: number
|
||||
ancestors: string[]
|
||||
} & DataSourceNotionPage
|
||||
|
||||
export type NotionPageTreeMap = Record<string, NotionPageTreeItem>
|
||||
|
||||
export type NotionPageRow = {
|
||||
page: DataSourceNotionPage
|
||||
parentExists: boolean
|
||||
depth: number
|
||||
expand: boolean
|
||||
hasChild: boolean
|
||||
ancestors: string[]
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import type { NotionPageSelectionMode } from './types'
|
||||
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
||||
import { startTransition, useCallback, useDeferredValue, useMemo, useState } from 'react'
|
||||
import { buildNotionPageTree, getNextSelectedPageIds, getRootPageIds, getVisiblePageRows } from './utils'
|
||||
|
||||
type UsePageSelectorModelProps = {
|
||||
checkedIds: Set<string>
|
||||
searchValue: string
|
||||
pagesMap: DataSourceNotionPageMap
|
||||
list: DataSourceNotionPage[]
|
||||
onSelect: (selectedPagesId: Set<string>) => void
|
||||
previewPageId?: string
|
||||
onPreview?: (selectedPageId: string) => void
|
||||
selectionMode: NotionPageSelectionMode
|
||||
}
|
||||
|
||||
export const usePageSelectorModel = ({
|
||||
checkedIds,
|
||||
searchValue,
|
||||
pagesMap,
|
||||
list,
|
||||
onSelect,
|
||||
previewPageId,
|
||||
onPreview,
|
||||
selectionMode,
|
||||
}: UsePageSelectorModelProps) => {
|
||||
const deferredSearchValue = useDeferredValue(searchValue)
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => new Set())
|
||||
const [localPreviewPageId, setLocalPreviewPageId] = useState('')
|
||||
|
||||
const treeMap = useMemo(() => buildNotionPageTree(list, pagesMap), [list, pagesMap])
|
||||
const rootPageIds = useMemo(() => getRootPageIds(list, pagesMap), [list, pagesMap])
|
||||
|
||||
const rows = useMemo(() => {
|
||||
return getVisiblePageRows({
|
||||
list,
|
||||
pagesMap,
|
||||
searchValue: deferredSearchValue,
|
||||
treeMap,
|
||||
rootPageIds,
|
||||
expandedIds,
|
||||
})
|
||||
}, [deferredSearchValue, expandedIds, list, pagesMap, rootPageIds, treeMap])
|
||||
|
||||
const currentPreviewPageId = previewPageId ?? localPreviewPageId
|
||||
|
||||
const handleToggle = useCallback((pageId: string) => {
|
||||
startTransition(() => {
|
||||
setExpandedIds((currentExpandedIds) => {
|
||||
const nextExpandedIds = new Set(currentExpandedIds)
|
||||
|
||||
if (nextExpandedIds.has(pageId)) {
|
||||
nextExpandedIds.delete(pageId)
|
||||
treeMap[pageId]?.descendants.forEach(descendantId => nextExpandedIds.delete(descendantId))
|
||||
}
|
||||
else {
|
||||
nextExpandedIds.add(pageId)
|
||||
}
|
||||
|
||||
return nextExpandedIds
|
||||
})
|
||||
})
|
||||
}, [treeMap])
|
||||
|
||||
const handleSelect = useCallback((pageId: string) => {
|
||||
onSelect(getNextSelectedPageIds({
|
||||
checkedIds,
|
||||
pageId,
|
||||
searchValue: deferredSearchValue,
|
||||
selectionMode,
|
||||
treeMap,
|
||||
}))
|
||||
}, [checkedIds, deferredSearchValue, onSelect, selectionMode, treeMap])
|
||||
|
||||
const handlePreview = useCallback((pageId: string) => {
|
||||
setLocalPreviewPageId(pageId)
|
||||
onPreview?.(pageId)
|
||||
}, [onPreview])
|
||||
|
||||
return {
|
||||
currentPreviewPageId,
|
||||
effectiveSearchValue: deferredSearchValue,
|
||||
rows,
|
||||
handlePreview,
|
||||
handleSelect,
|
||||
handleToggle,
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import type { NotionPageRow, NotionPageSelectionMode, NotionPageTreeItem, NotionPageTreeMap } from './types'
|
||||
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
||||
|
||||
export const recursivePushInParentDescendants = (
|
||||
pagesMap: DataSourceNotionPageMap,
|
||||
listTreeMap: NotionPageTreeMap,
|
||||
current: NotionPageTreeItem,
|
||||
leafItem: NotionPageTreeItem,
|
||||
) => {
|
||||
const parentId = current.parent_id
|
||||
const pageId = current.page_id
|
||||
|
||||
if (!parentId || !pageId)
|
||||
return
|
||||
|
||||
if (parentId !== 'root' && pagesMap[parentId]) {
|
||||
if (!listTreeMap[parentId]) {
|
||||
const children = new Set([pageId])
|
||||
const descendants = new Set([pageId, leafItem.page_id])
|
||||
listTreeMap[parentId] = {
|
||||
...pagesMap[parentId],
|
||||
children,
|
||||
descendants,
|
||||
depth: 0,
|
||||
ancestors: [],
|
||||
}
|
||||
}
|
||||
else {
|
||||
listTreeMap[parentId].children.add(pageId)
|
||||
listTreeMap[parentId].descendants.add(pageId)
|
||||
listTreeMap[parentId].descendants.add(leafItem.page_id)
|
||||
}
|
||||
|
||||
leafItem.depth++
|
||||
leafItem.ancestors.unshift(listTreeMap[parentId].page_name)
|
||||
|
||||
if (listTreeMap[parentId].parent_id !== 'root')
|
||||
recursivePushInParentDescendants(pagesMap, listTreeMap, listTreeMap[parentId], leafItem)
|
||||
}
|
||||
}
|
||||
|
||||
export const buildNotionPageTree = (
|
||||
list: DataSourceNotionPage[],
|
||||
pagesMap: DataSourceNotionPageMap,
|
||||
): NotionPageTreeMap => {
|
||||
return list.reduce((prev: NotionPageTreeMap, next) => {
|
||||
const pageId = next.page_id
|
||||
if (!prev[pageId])
|
||||
prev[pageId] = { ...next, children: new Set(), descendants: new Set(), depth: 0, ancestors: [] }
|
||||
|
||||
recursivePushInParentDescendants(pagesMap, prev, prev[pageId], prev[pageId])
|
||||
return prev
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const getRootPageIds = (
|
||||
list: DataSourceNotionPage[],
|
||||
pagesMap: DataSourceNotionPageMap,
|
||||
) => {
|
||||
return list
|
||||
.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id])
|
||||
.map(item => item.page_id)
|
||||
}
|
||||
|
||||
export const getVisiblePageRows = ({
|
||||
list,
|
||||
pagesMap,
|
||||
searchValue,
|
||||
treeMap,
|
||||
rootPageIds,
|
||||
expandedIds,
|
||||
}: {
|
||||
list: DataSourceNotionPage[]
|
||||
pagesMap: DataSourceNotionPageMap
|
||||
searchValue: string
|
||||
treeMap: NotionPageTreeMap
|
||||
rootPageIds: string[]
|
||||
expandedIds: Set<string>
|
||||
}): NotionPageRow[] => {
|
||||
if (searchValue) {
|
||||
return list
|
||||
.filter(item => item.page_name.includes(searchValue))
|
||||
.map(item => ({
|
||||
page: item,
|
||||
parentExists: item.parent_id !== 'root' && Boolean(pagesMap[item.parent_id]),
|
||||
depth: treeMap[item.page_id]?.depth ?? 0,
|
||||
expand: false,
|
||||
hasChild: (treeMap[item.page_id]?.children.size ?? 0) > 0,
|
||||
ancestors: treeMap[item.page_id]?.ancestors ?? [],
|
||||
}))
|
||||
}
|
||||
|
||||
const rows: NotionPageRow[] = []
|
||||
|
||||
const visit = (pageId: string) => {
|
||||
const current = treeMap[pageId]
|
||||
if (!current)
|
||||
return
|
||||
|
||||
const expand = expandedIds.has(pageId)
|
||||
rows.push({
|
||||
page: current,
|
||||
parentExists: current.parent_id !== 'root' && Boolean(pagesMap[current.parent_id]),
|
||||
depth: current.depth,
|
||||
expand,
|
||||
hasChild: current.children.size > 0,
|
||||
ancestors: current.ancestors,
|
||||
})
|
||||
|
||||
if (!expand)
|
||||
return
|
||||
|
||||
current.children.forEach(visit)
|
||||
}
|
||||
|
||||
rootPageIds.forEach(visit)
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
export const getNextSelectedPageIds = ({
|
||||
checkedIds,
|
||||
pageId,
|
||||
searchValue,
|
||||
selectionMode,
|
||||
treeMap,
|
||||
}: {
|
||||
checkedIds: Set<string>
|
||||
pageId: string
|
||||
searchValue: string
|
||||
selectionMode: NotionPageSelectionMode
|
||||
treeMap: NotionPageTreeMap
|
||||
}) => {
|
||||
const nextCheckedIds = new Set(checkedIds)
|
||||
const descendants = treeMap[pageId]?.descendants ?? new Set<string>()
|
||||
|
||||
if (selectionMode === 'single') {
|
||||
if (nextCheckedIds.has(pageId)) {
|
||||
nextCheckedIds.delete(pageId)
|
||||
}
|
||||
else {
|
||||
nextCheckedIds.clear()
|
||||
nextCheckedIds.add(pageId)
|
||||
}
|
||||
|
||||
return nextCheckedIds
|
||||
}
|
||||
|
||||
if (nextCheckedIds.has(pageId)) {
|
||||
if (!searchValue)
|
||||
descendants.forEach(item => nextCheckedIds.delete(item))
|
||||
|
||||
nextCheckedIds.delete(pageId)
|
||||
return nextCheckedIds
|
||||
}
|
||||
|
||||
if (!searchValue)
|
||||
descendants.forEach(item => nextCheckedIds.add(item))
|
||||
|
||||
nextCheckedIds.add(pageId)
|
||||
|
||||
return nextCheckedIds
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { NotionPageRow, NotionPageSelectionMode } from './types'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { useRef } from 'react'
|
||||
import PageRow from './page-row'
|
||||
|
||||
type VirtualPageListProps = {
|
||||
checkedIds: Set<string>
|
||||
disabledValue: Set<string>
|
||||
onPreview: (pageId: string) => void
|
||||
onSelect: (pageId: string) => void
|
||||
onToggle: (pageId: string) => void
|
||||
previewPageId: string
|
||||
rows: NotionPageRow[]
|
||||
searchValue: string
|
||||
selectionMode: NotionPageSelectionMode
|
||||
showPreview: boolean
|
||||
}
|
||||
|
||||
const rowHeight = 28
|
||||
|
||||
const VirtualPageList = ({
|
||||
checkedIds,
|
||||
disabledValue,
|
||||
onPreview,
|
||||
onSelect,
|
||||
onToggle,
|
||||
previewPageId,
|
||||
rows,
|
||||
searchValue,
|
||||
selectionMode,
|
||||
showPreview,
|
||||
}: VirtualPageListProps) => {
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: rows.length,
|
||||
estimateSize: () => rowHeight,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
overscan: 6,
|
||||
paddingEnd: 8,
|
||||
paddingStart: 8,
|
||||
})
|
||||
|
||||
const virtualRows = rowVirtualizer.getVirtualItems()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="h-[296px] overflow-auto"
|
||||
data-testid="virtual-list"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{virtualRows.map((virtualRow) => {
|
||||
const row = rows[virtualRow.index]
|
||||
const pageId = row.page.page_id
|
||||
|
||||
return (
|
||||
<PageRow
|
||||
key={pageId}
|
||||
checked={checkedIds.has(pageId)}
|
||||
disabled={disabledValue.has(pageId)}
|
||||
isPreviewed={previewPageId === pageId}
|
||||
onPreview={onPreview}
|
||||
onSelect={onSelect}
|
||||
onToggle={onToggle}
|
||||
row={row}
|
||||
searchValue={searchValue}
|
||||
selectionMode={selectionMode}
|
||||
showPreview={showPreview}
|
||||
style={{
|
||||
height: `${virtualRow.size}px`,
|
||||
left: 8,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
width: 'calc(100% - 16px)',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VirtualPageList
|
||||
@@ -6,7 +6,7 @@
|
||||
*
|
||||
* Migration guide:
|
||||
* - Tooltip → `@/app/components/base/ui/tooltip`
|
||||
* - Menu/Dropdown → `@/app/components/base/ui/dropdown-menu`
|
||||
* - Menu/Dropdown → `@langgenius/dify-ui/dropdown-menu`
|
||||
* - Popover → `@/app/components/base/ui/popover`
|
||||
* - Dialog/Modal → `@/app/components/base/ui/dialog`
|
||||
* - Select → `@/app/components/base/ui/select`
|
||||
|
||||
@@ -10,12 +10,6 @@ import { cn } from '@/utils/classnames'
|
||||
|
||||
export const Select = BaseSelect.Root
|
||||
export const SelectValue = BaseSelect.Value
|
||||
/** @public */
|
||||
export const SelectGroup = BaseSelect.Group
|
||||
/** @public */
|
||||
export const SelectGroupLabel = BaseSelect.GroupLabel
|
||||
/** @public */
|
||||
export const SelectSeparator = BaseSelect.Separator
|
||||
|
||||
const selectTriggerVariants = cva(
|
||||
'',
|
||||
|
||||
@@ -106,7 +106,7 @@ const OnlineDocuments = ({
|
||||
if (!currentCredentialId)
|
||||
return
|
||||
getOnlineDocuments()
|
||||
}, [currentCredentialId, getOnlineDocuments])
|
||||
}, [currentCredentialId])
|
||||
|
||||
const handleSearchValueChange = useCallback((value: string) => {
|
||||
const { setSearchValue } = dataSourceStore.getState()
|
||||
@@ -156,7 +156,6 @@ const OnlineDocuments = ({
|
||||
{documentsData?.length
|
||||
? (
|
||||
<PageSelector
|
||||
key={`${currentCredentialId}:${supportBatchUpload ? 'multiple' : 'single'}`}
|
||||
checkedIds={selectedPagesId}
|
||||
disabledValue={new Set()}
|
||||
searchValue={searchValue}
|
||||
@@ -166,6 +165,7 @@ const OnlineDocuments = ({
|
||||
canPreview={!isInPipeline}
|
||||
onPreview={handlePreviewPage}
|
||||
isMultipleChoice={supportBatchUpload}
|
||||
currentCredentialId={currentCredentialId}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
import type { NotionPageTreeItem, NotionPageTreeMap } from '@/app/components/base/notion-page-selector/page-selector/types'
|
||||
import type { NotionPageTreeItem, NotionPageTreeMap } from '../index'
|
||||
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { recursivePushInParentDescendants } from '@/app/components/base/notion-page-selector/page-selector/utils'
|
||||
import PageSelector from '../index'
|
||||
import { recursivePushInParentDescendants } from '../utils'
|
||||
|
||||
vi.mock('@tanstack/react-virtual')
|
||||
// Mock react-window FixedSizeList - renders items directly for testing
|
||||
vi.mock('react-window', () => ({
|
||||
FixedSizeList: ({ children: ItemComponent, itemCount, itemData, itemKey }: { children: React.ComponentType<{ index: number, style: React.CSSProperties, data: unknown }>, itemCount: number, itemData: unknown, itemKey?: (index: number, data: unknown) => string | number }) => (
|
||||
<div data-testid="virtual-list">
|
||||
{Array.from({ length: itemCount }).map((_, index) => (
|
||||
<ItemComponent
|
||||
key={itemKey?.(index, itemData) || index}
|
||||
index={index}
|
||||
style={{ top: index * 28, left: 0, right: 0, width: '100%', position: 'absolute' as const }}
|
||||
data={itemData}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
areEqual: (prevProps: Record<string, unknown>, nextProps: Record<string, unknown>) => prevProps === nextProps,
|
||||
}))
|
||||
|
||||
// Note: NotionIcon from @/app/components/base/ is NOT mocked - using real component per testing guidelines
|
||||
|
||||
@@ -55,6 +70,7 @@ const createDefaultProps = (overrides?: Partial<PageSelectorProps>): PageSelecto
|
||||
canPreview: true,
|
||||
onPreview: vi.fn(),
|
||||
isMultipleChoice: true,
|
||||
currentCredentialId: 'cred-1',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
@@ -98,7 +114,7 @@ describe('PageSelector', () => {
|
||||
expect(screen.queryByTestId('virtual-list')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render items using VirtualList', () => {
|
||||
it('should render items using FixedSizeList', () => {
|
||||
const pages = [
|
||||
createMockPage({ page_id: 'page-1', page_name: 'Page 1' }),
|
||||
createMockPage({ page_id: 'page-2', page_name: 'Page 2' }),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NotionPageTreeItem, NotionPageTreeMap } from '@/app/components/base/notion-page-selector/page-selector/types'
|
||||
import type { NotionPageTreeItem, NotionPageTreeMap } from '../index'
|
||||
import type { DataSourceNotionPageMap } from '@/models/common'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { recursivePushInParentDescendants } from '@/app/components/base/notion-page-selector/page-selector/utils'
|
||||
import { recursivePushInParentDescendants } from '../utils'
|
||||
|
||||
const makePageEntry = (overrides: Partial<NotionPageTreeItem>): NotionPageTreeItem => ({
|
||||
page_icon: null,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePageSelectorModel } from '@/app/components/base/notion-page-selector/page-selector/use-page-selector-model'
|
||||
import VirtualPageList from '@/app/components/base/notion-page-selector/page-selector/virtual-page-list'
|
||||
import { FixedSizeList as List } from 'react-window'
|
||||
import Item from './item'
|
||||
import { recursivePushInParentDescendants } from './utils'
|
||||
|
||||
type PageSelectorProps = {
|
||||
checkedIds: Set<string>
|
||||
@@ -13,9 +15,23 @@ type PageSelectorProps = {
|
||||
canPreview?: boolean
|
||||
onPreview?: (selectedPageId: string) => void
|
||||
isMultipleChoice?: boolean
|
||||
currentCredentialId?: string
|
||||
currentCredentialId: string
|
||||
}
|
||||
|
||||
export type NotionPageTreeItem = {
|
||||
children: Set<string>
|
||||
descendants: Set<string>
|
||||
depth: number
|
||||
ancestors: string[]
|
||||
} & DataSourceNotionPage
|
||||
|
||||
export type NotionPageTreeMap = Record<string, NotionPageTreeItem>
|
||||
|
||||
type NotionPageItem = {
|
||||
expand: boolean
|
||||
depth: number
|
||||
} & DataSourceNotionPage
|
||||
|
||||
const PageSelector = ({
|
||||
checkedIds,
|
||||
disabledValue,
|
||||
@@ -26,28 +42,116 @@ const PageSelector = ({
|
||||
canPreview = true,
|
||||
onPreview,
|
||||
isMultipleChoice = true,
|
||||
currentCredentialId: _currentCredentialId,
|
||||
currentCredentialId,
|
||||
}: PageSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const selectionMode = isMultipleChoice ? 'multiple' : 'single'
|
||||
const {
|
||||
currentPreviewPageId,
|
||||
effectiveSearchValue,
|
||||
rows,
|
||||
handlePreview,
|
||||
handleSelect,
|
||||
handleToggle,
|
||||
} = usePageSelectorModel({
|
||||
checkedIds,
|
||||
list,
|
||||
onPreview,
|
||||
onSelect,
|
||||
pagesMap,
|
||||
searchValue,
|
||||
selectionMode,
|
||||
})
|
||||
const [dataList, setDataList] = useState<NotionPageItem[]>([])
|
||||
const [currentPreviewPageId, setCurrentPreviewPageId] = useState('')
|
||||
|
||||
if (!rows.length) {
|
||||
useEffect(() => {
|
||||
setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
expand: false,
|
||||
depth: 0,
|
||||
}
|
||||
}))
|
||||
}, [currentCredentialId])
|
||||
|
||||
const searchDataList = list.filter((item) => {
|
||||
return item.page_name.includes(searchValue)
|
||||
}).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
expand: false,
|
||||
depth: 0,
|
||||
}
|
||||
})
|
||||
const currentDataList = searchValue ? searchDataList : dataList
|
||||
|
||||
const listMapWithChildrenAndDescendants = useMemo(() => {
|
||||
return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
|
||||
const pageId = next.page_id
|
||||
if (!prev[pageId])
|
||||
prev[pageId] = { ...next, children: new Set(), descendants: new Set(), depth: 0, ancestors: [] }
|
||||
|
||||
recursivePushInParentDescendants(pagesMap, prev, prev[pageId], prev[pageId])
|
||||
return prev
|
||||
}, {})
|
||||
}, [list, pagesMap])
|
||||
|
||||
const handleToggle = useCallback((index: number) => {
|
||||
const current = dataList[index]
|
||||
const pageId = current.page_id
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
||||
const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants)
|
||||
const childrenIds = Array.from(currentWithChildrenAndDescendants.children)
|
||||
let newDataList = []
|
||||
|
||||
if (current.expand) {
|
||||
current.expand = false
|
||||
|
||||
newDataList = dataList.filter(item => !descendantsIds.includes(item.page_id))
|
||||
}
|
||||
else {
|
||||
current.expand = true
|
||||
|
||||
newDataList = [
|
||||
...dataList.slice(0, index + 1),
|
||||
...childrenIds.map(item => ({
|
||||
...pagesMap[item],
|
||||
expand: false,
|
||||
depth: listMapWithChildrenAndDescendants[item].depth,
|
||||
})),
|
||||
...dataList.slice(index + 1),
|
||||
]
|
||||
}
|
||||
setDataList(newDataList)
|
||||
}, [dataList, listMapWithChildrenAndDescendants, pagesMap])
|
||||
|
||||
const handleCheck = useCallback((index: number) => {
|
||||
const copyValue = new Set(checkedIds)
|
||||
const current = currentDataList[index]
|
||||
const pageId = current.page_id
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
||||
|
||||
if (copyValue.has(pageId)) {
|
||||
if (!searchValue && isMultipleChoice) {
|
||||
for (const item of currentWithChildrenAndDescendants.descendants)
|
||||
copyValue.delete(item)
|
||||
}
|
||||
|
||||
copyValue.delete(pageId)
|
||||
}
|
||||
else {
|
||||
if (!searchValue && isMultipleChoice) {
|
||||
for (const item of currentWithChildrenAndDescendants.descendants)
|
||||
copyValue.add(item)
|
||||
}
|
||||
// Single choice mode, clear previous selection
|
||||
if (!isMultipleChoice && copyValue.size > 0) {
|
||||
copyValue.clear()
|
||||
copyValue.add(pageId)
|
||||
}
|
||||
else {
|
||||
copyValue.add(pageId)
|
||||
}
|
||||
}
|
||||
|
||||
onSelect(new Set(copyValue))
|
||||
}, [currentDataList, isMultipleChoice, listMapWithChildrenAndDescendants, onSelect, searchValue, checkedIds])
|
||||
|
||||
const handlePreview = useCallback((index: number) => {
|
||||
const current = currentDataList[index]
|
||||
const pageId = current.page_id
|
||||
|
||||
setCurrentPreviewPageId(pageId)
|
||||
|
||||
if (onPreview)
|
||||
onPreview(pageId)
|
||||
}, [currentDataList, onPreview])
|
||||
|
||||
if (!currentDataList.length) {
|
||||
return (
|
||||
<div className="flex h-[296px] items-center justify-center text-[13px] text-text-tertiary">
|
||||
{t('dataSource.notion.selector.noSearchResult', { ns: 'common' })}
|
||||
@@ -56,18 +160,30 @@ const PageSelector = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualPageList
|
||||
checkedIds={checkedIds}
|
||||
disabledValue={disabledValue}
|
||||
onPreview={handlePreview}
|
||||
onSelect={handleSelect}
|
||||
onToggle={handleToggle}
|
||||
previewPageId={currentPreviewPageId}
|
||||
rows={rows}
|
||||
searchValue={effectiveSearchValue}
|
||||
selectionMode={selectionMode}
|
||||
showPreview={canPreview}
|
||||
/>
|
||||
<List
|
||||
className="py-2"
|
||||
height={296}
|
||||
itemCount={currentDataList.length}
|
||||
itemSize={28}
|
||||
width="100%"
|
||||
itemKey={(index, data) => data.dataList[index].page_id}
|
||||
itemData={{
|
||||
dataList: currentDataList,
|
||||
handleToggle,
|
||||
checkedIds,
|
||||
disabledCheckedIds: disabledValue,
|
||||
handleCheck,
|
||||
canPreview,
|
||||
handlePreview,
|
||||
listMapWithChildrenAndDescendants,
|
||||
searchValue,
|
||||
previewPageId: currentPreviewPageId,
|
||||
pagesMap,
|
||||
isMultipleChoice,
|
||||
}}
|
||||
>
|
||||
{Item}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import type { ListChildComponentProps } from 'react-window'
|
||||
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { areEqual } from 'react-window'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import NotionIcon from '@/app/components/base/notion-icon'
|
||||
import Radio from '@/app/components/base/radio/ui'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type NotionPageTreeItem = {
|
||||
children: Set<string>
|
||||
descendants: Set<string>
|
||||
depth: number
|
||||
ancestors: string[]
|
||||
} & DataSourceNotionPage
|
||||
|
||||
type NotionPageTreeMap = Record<string, NotionPageTreeItem>
|
||||
|
||||
type NotionPageItem = {
|
||||
expand: boolean
|
||||
depth: number
|
||||
} & DataSourceNotionPage
|
||||
|
||||
const Item = ({ index, style, data }: ListChildComponentProps<{
|
||||
dataList: NotionPageItem[]
|
||||
handleToggle: (index: number) => void
|
||||
checkedIds: Set<string>
|
||||
disabledCheckedIds: Set<string>
|
||||
handleCheck: (index: number) => void
|
||||
canPreview?: boolean
|
||||
handlePreview: (index: number) => void
|
||||
listMapWithChildrenAndDescendants: NotionPageTreeMap
|
||||
searchValue: string
|
||||
previewPageId: string
|
||||
pagesMap: DataSourceNotionPageMap
|
||||
isMultipleChoice?: boolean
|
||||
}>) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
dataList,
|
||||
handleToggle,
|
||||
checkedIds,
|
||||
disabledCheckedIds,
|
||||
handleCheck,
|
||||
canPreview,
|
||||
handlePreview,
|
||||
listMapWithChildrenAndDescendants,
|
||||
searchValue,
|
||||
previewPageId,
|
||||
pagesMap,
|
||||
isMultipleChoice,
|
||||
} = data
|
||||
const current = dataList[index]
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id]
|
||||
const hasChild = currentWithChildrenAndDescendants.descendants.size > 0
|
||||
const ancestors = currentWithChildrenAndDescendants.ancestors
|
||||
const breadCrumbs = ancestors.length ? [...ancestors, current.page_name] : [current.page_name]
|
||||
const disabled = disabledCheckedIds.has(current.page_id)
|
||||
|
||||
const renderArrow = () => {
|
||||
if (hasChild) {
|
||||
return (
|
||||
<div
|
||||
className="mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover"
|
||||
style={{ marginLeft: current.depth * 8 }}
|
||||
onClick={() => handleToggle(index)}
|
||||
>
|
||||
{
|
||||
current.expand
|
||||
? <RiArrowDownSLine className="h-4 w-4 text-text-tertiary" />
|
||||
: <RiArrowRightSLine className="h-4 w-4 text-text-tertiary" />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (current.parent_id === 'root' || !pagesMap[current.parent_id]) {
|
||||
return (
|
||||
<div></div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="mr-1 h-5 w-5 shrink-0" style={{ marginLeft: current.depth * 8 }} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group flex cursor-pointer items-center rounded-md pl-2 pr-[2px] hover:bg-state-base-hover', previewPageId === current.page_id && 'bg-state-base-hover')}
|
||||
style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }}
|
||||
>
|
||||
{isMultipleChoice
|
||||
? (
|
||||
<Checkbox
|
||||
className="mr-2 shrink-0"
|
||||
checked={checkedIds.has(current.page_id)}
|
||||
disabled={disabled}
|
||||
onCheck={() => {
|
||||
handleCheck(index)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Radio
|
||||
className="mr-2 shrink-0"
|
||||
isChecked={checkedIds.has(current.page_id)}
|
||||
disabled={disabled}
|
||||
onCheck={() => {
|
||||
handleCheck(index)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!searchValue && renderArrow()}
|
||||
<NotionIcon
|
||||
className="mr-1 shrink-0"
|
||||
type="page"
|
||||
src={current.page_icon}
|
||||
/>
|
||||
<div
|
||||
className="grow truncate text-[13px] font-medium leading-4 text-text-secondary"
|
||||
title={current.page_name}
|
||||
>
|
||||
{current.page_name}
|
||||
</div>
|
||||
{
|
||||
canPreview && (
|
||||
<div
|
||||
className="ml-1 hidden h-6 shrink-0 cursor-pointer items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-xs
|
||||
font-medium leading-4 text-components-button-secondary-text shadow-xs shadow-shadow-shadow-3 backdrop-blur-[10px]
|
||||
hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover group-hover:flex"
|
||||
onClick={() => handlePreview(index)}
|
||||
>
|
||||
{t('dataSource.notion.selector.preview', { ns: 'common' })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
searchValue && (
|
||||
<div
|
||||
className="ml-1 max-w-[120px] shrink-0 truncate text-xs text-text-quaternary"
|
||||
title={breadCrumbs.join(' / ')}
|
||||
>
|
||||
{breadCrumbs.join(' / ')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Item, areEqual)
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { NotionPageTreeItem, NotionPageTreeMap } from './index'
|
||||
import type { DataSourceNotionPageMap } from '@/models/common'
|
||||
|
||||
export const recursivePushInParentDescendants = (
|
||||
pagesMap: DataSourceNotionPageMap,
|
||||
listTreeMap: NotionPageTreeMap,
|
||||
current: NotionPageTreeItem,
|
||||
leafItem: NotionPageTreeItem,
|
||||
) => {
|
||||
const parentId = current.parent_id
|
||||
const pageId = current.page_id
|
||||
|
||||
if (!parentId || !pageId)
|
||||
return
|
||||
|
||||
if (parentId !== 'root' && pagesMap[parentId]) {
|
||||
if (!listTreeMap[parentId]) {
|
||||
const children = new Set([pageId])
|
||||
const descendants = new Set([pageId, leafItem.page_id])
|
||||
listTreeMap[parentId] = {
|
||||
...pagesMap[parentId],
|
||||
children,
|
||||
descendants,
|
||||
depth: 0,
|
||||
ancestors: [],
|
||||
}
|
||||
}
|
||||
else {
|
||||
listTreeMap[parentId].children.add(pageId)
|
||||
listTreeMap[parentId].descendants.add(pageId)
|
||||
listTreeMap[parentId].descendants.add(leafItem.page_id)
|
||||
}
|
||||
leafItem.depth++
|
||||
leafItem.ancestors.unshift(listTreeMap[parentId].page_name)
|
||||
|
||||
if (listTreeMap[parentId].parent_id !== 'root')
|
||||
recursivePushInParentDescendants(pagesMap, listTreeMap, listTreeMap[parentId], leafItem)
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ vi.mock('@/app/components/datasets/common/image-list', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Markdown uses next/dynamic and shiki (ESM)
|
||||
// Markdown uses next/dynamic and react-syntax-highlighter (ESM)
|
||||
vi.mock('@/app/components/base/markdown', () => ({
|
||||
Markdown: ({ content, className }: { content: string, className?: string }) => (
|
||||
<div data-testid="markdown" className={`markdown-body ${className || ''}`}>{content}</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ModalContextState } from '@/context/modal-context'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/app/components/base/ui/dropdown-menu'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AppContextValue } from '@/context/app-context'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/app/components/base/ui/dropdown-menu'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/app/components/base/ui/dropdown-menu'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import type { MouseEventHandler, ReactNode } from 'react'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import ThemeSwitcher from '@/app/components/base/theme-switcher'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/app/components/base/ui/dropdown-menu'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/app/components/base/ui/dropdown-menu'
|
||||
import { toggleZendeskWindow } from '@/app/components/base/zendesk/utils'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { SUPPORT_EMAIL_ADDRESS, ZENDESK_WIDGET_KEY } from '@/config'
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Brush01 } from '@/app/components/base/icons/src/vender/solid/editor'
|
||||
import { Scales02 } from '@/app/components/base/icons/src/vender/solid/FinanceAndECommerce'
|
||||
import { Target04 } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/app/components/base/ui/dropdown-menu'
|
||||
import { TONE_LIST } from '@/config'
|
||||
|
||||
const toneI18nKeyMap = {
|
||||
|
||||
@@ -14,7 +14,7 @@ vi.mock('@/utils/classnames', () => ({
|
||||
cn: (...args: (string | undefined | false | null)[]) => args.filter(Boolean).join(' '),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/dropdown-menu', () => ({
|
||||
vi.mock('@langgenius/dify-ui/dropdown-menu', () => ({
|
||||
DropdownMenu: ({ children, open }: { children: ReactNode, open: boolean }) => (
|
||||
<div data-testid="dropdown-menu" data-open={open}>{children}</div>
|
||||
),
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
'use client'
|
||||
import type { Placement } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import type { FC } from 'react'
|
||||
import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/app/components/base/ui/dropdown-menu'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { PluginSource } from '../types'
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
} from '@langgenius/dify-ui/context-menu'
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEdges } from 'reactflow'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
} from '@/app/components/base/ui/context-menu'
|
||||
import { useEdgesInteractions, usePanelInteractions } from './hooks'
|
||||
import ShortcutsName from './shortcuts-name'
|
||||
import { useStore } from './store'
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import type { Node } from './types'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuGroup,
|
||||
ContextMenuGroupLabel,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
} from '@langgenius/dify-ui/context-menu'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
memo,
|
||||
@@ -8,14 +16,6 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useReactFlowStore, useStoreApi } from 'reactflow'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuGroup,
|
||||
ContextMenuGroupLabel,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
} from '@/app/components/base/ui/context-menu'
|
||||
import { useNodesInteractions, useNodesReadOnly, useNodesSyncDraft } from './hooks'
|
||||
import { useSelectionInteractions } from './hooks/use-selection-interactions'
|
||||
import { useWorkflowHistory, WorkflowHistoryEvent } from './hooks/use-workflow-history'
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
@import './preflight.css' layer(base);
|
||||
|
||||
@import '../../themes/light.css' layer(base);
|
||||
@import '../../themes/dark.css' layer(base);
|
||||
@import '../../themes/manual-light.css' layer(base);
|
||||
@import '../../themes/manual-dark.css' layer(base);
|
||||
@import '@langgenius/dify-ui/styles.css';
|
||||
@import './monaco-sticky-fix.css' layer(base);
|
||||
|
||||
@import '../components/base/action-button/index.css';
|
||||
@@ -17,727 +13,6 @@
|
||||
|
||||
@config '../../tailwind.config.ts';
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
|
||||
@utility system-kbd {
|
||||
/* font define start */
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-regular-uppercase {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-regular {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-medium {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-medium-uppercase {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-2xs-semibold-uppercase {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
line-height: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-regular {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-regular-uppercase {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-medium {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-medium-uppercase {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-semibold {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xs-semibold-uppercase {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-regular {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-medium {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-medium-uppercase {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-semibold {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-sm-semibold-uppercase {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-md-regular {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-md-medium {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-md-semibold {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-md-semibold-uppercase {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xl-regular {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xl-medium {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility system-xl-semibold {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-xs-regular {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-xs-semibold {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-sm-regular {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-sm-semibold {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-md-regular {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility code-md-semibold {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xs-light {
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xs-regular {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xs-medium {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-sm-light {
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-sm-regular {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-sm-medium {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-md-light {
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-md-regular {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-md-medium {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-lg-light {
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-lg-regular {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-lg-medium {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xl-regular {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xl-medium {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-xl-light {
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
line-height: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-2xl-light {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-2xl-regular {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility body-2xl-medium {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-xs-semi-bold {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-xs-bold {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-sm-semi-bold {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-sm-bold {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-md-semi-bold {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-md-bold {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-lg-semi-bold {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-lg-bold {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-xl-semi-bold {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-xl-bold {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-2xl-semi-bold {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-2xl-bold {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-3xl-semi-bold {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-3xl-bold {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-4xl-semi-bold {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-4xl-bold {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-5xl-semi-bold {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-5xl-bold {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-6xl-semi-bold {
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-6xl-bold {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-7xl-semi-bold {
|
||||
font-size: 48px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-7xl-bold {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-8xl-semi-bold {
|
||||
font-size: 60px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility title-8xl-bold {
|
||||
font-size: 60px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-2xs {
|
||||
/* font define end */
|
||||
|
||||
/* border radius start */
|
||||
border-radius: 2px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-xs {
|
||||
border-radius: 4px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-sm {
|
||||
border-radius: 6px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-md {
|
||||
border-radius: 8px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-lg {
|
||||
border-radius: 10px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-xl {
|
||||
border-radius: 12px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-2xl {
|
||||
border-radius: 16px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-3xl {
|
||||
border-radius: 20px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-4xl {
|
||||
border-radius: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-5xl {
|
||||
border-radius: 24px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-6xl {
|
||||
border-radius: 28px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-7xl {
|
||||
border-radius: 32px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-8xl {
|
||||
border-radius: 40px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-9xl {
|
||||
border-radius: 48px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility radius-full {
|
||||
border-radius: 64px;
|
||||
|
||||
/* border radius end */
|
||||
}
|
||||
|
||||
@utility no-scrollbar {
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
@utility no-spinner {
|
||||
/* Hide arrows from number input */
|
||||
&::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
html {
|
||||
color-scheme: light;
|
||||
@@ -794,35 +69,6 @@
|
||||
--card-border-rgb: 131, 134, 135;
|
||||
}
|
||||
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
|
||||
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
|
||||
--secondary-glow: linear-gradient(to bottom right,
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0.3));
|
||||
|
||||
--tile-start-rgb: 2, 13, 46;
|
||||
--tile-end-rgb: 2, 5, 19;
|
||||
--tile-border: conic-gradient(#ffffff80,
|
||||
#ffffff40,
|
||||
#ffffff30,
|
||||
#ffffff20,
|
||||
#ffffff10,
|
||||
#ffffff10,
|
||||
#ffffff80);
|
||||
|
||||
--callout-rgb: 20, 20, 20;
|
||||
--callout-border-rgb: 108, 108, 108;
|
||||
--card-rgb: 100, 100, 100;
|
||||
--card-border-rgb: 200, 200, 200;
|
||||
}
|
||||
} */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
@@ -838,12 +84,6 @@
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
user-select: none;
|
||||
/* background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb)); */
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -852,13 +92,6 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
} */
|
||||
|
||||
/* CSS Utils */
|
||||
.h1 {
|
||||
padding-bottom: 1.5rem;
|
||||
line-height: 1.5;
|
||||
@@ -880,7 +113,7 @@
|
||||
|
||||
@layer components {
|
||||
.link {
|
||||
@apply text-blue-600 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out;
|
||||
@apply cursor-pointer text-blue-600 transition-opacity duration-200 ease-in-out hover:opacity-80;
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
@@ -891,13 +124,11 @@
|
||||
text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* overwrite paging active dark model style */
|
||||
[class*='style_paginatio'] li .text-primary-600 {
|
||||
color: rgb(28 100 242);
|
||||
background-color: rgb(235 245 255);
|
||||
}
|
||||
|
||||
/* support safari 14 and below */
|
||||
.inset-0 {
|
||||
left: 0;
|
||||
right: 0;
|
||||
@@ -909,19 +140,4 @@
|
||||
[data-theme='light'] [data-hide-on-theme='light'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Shiki code block line numbers */
|
||||
.shiki-line-numbers code {
|
||||
counter-reset: line;
|
||||
}
|
||||
.shiki-line-numbers .line::before {
|
||||
counter-increment: line;
|
||||
content: counter(line);
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
margin-right: 0.75rem;
|
||||
text-align: right;
|
||||
color: var(--color-text-quaternary);
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import '../../themes/markdown-light.css';
|
||||
@import '../../themes/markdown-dark.css';
|
||||
@import '@langgenius/dify-ui/markdown.css';
|
||||
@reference "./globals.css";
|
||||
|
||||
.markdown-body {
|
||||
-ms-text-size-adjust: 100%;
|
||||
@@ -1070,6 +1070,9 @@
|
||||
filter: invert(50%);
|
||||
}
|
||||
|
||||
.markdown-body .react-syntax-highlighter-line-number {
|
||||
color: var(--color-text-quaternary);
|
||||
}
|
||||
.markdown-body .abcjs-inline-audio .abcjs-btn {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ This document tracks the migration away from legacy overlay APIs.
|
||||
- `@/app/components/base/toast` (including `context`)
|
||||
- Replacement primitives:
|
||||
- `@/app/components/base/ui/tooltip`
|
||||
- `@/app/components/base/ui/dropdown-menu`
|
||||
- `@/app/components/base/ui/context-menu`
|
||||
- `@langgenius/dify-ui/dropdown-menu`
|
||||
- `@langgenius/dify-ui/context-menu`
|
||||
- `@/app/components/base/ui/popover`
|
||||
- `@/app/components/base/ui/dialog`
|
||||
- `@/app/components/base/ui/alert-dialog`
|
||||
|
||||
@@ -10,7 +10,7 @@ When I ask you to write/refactor/fix tests, follow these rules by default.
|
||||
- **Testing Tools**: Vitest 4.0.16 + React Testing Library 16.0
|
||||
- **Test Environment**: happy-dom
|
||||
- **File Naming**: `ComponentName.spec.tsx` inside a same-level `__tests__/` directory
|
||||
- **Placement Rule**: Component, hook, and utility tests must live in a sibling `__tests__/` folder at the same level as the source under test. For example, `foo/index.tsx` maps to `foo/__tests__/index.spec.tsx`, and `foo/bar.ts` maps to `foo/__tests__/bar.spec.ts`.
|
||||
- **Placement Rule**: Component, hook, and utility tests must live in a sibling `__tests__/` folder at the same level as the source under test. For example, `foo/index.tsx` maps to `foo/__tests__/index.spec.tsx`, and `foo/bar.ts` maps to `foo/__tests__/bar.spec.ts`. This rule also applies to workspace packages under `packages/`.
|
||||
|
||||
## Running Tests
|
||||
|
||||
|
||||
@@ -3142,6 +3142,9 @@
|
||||
"react/set-state-in-effect": {
|
||||
"count": 7
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 9
|
||||
}
|
||||
@@ -3328,6 +3331,11 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/base/notion-page-selector/base.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/base/notion-page-selector/credential-selector/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 2
|
||||
@@ -3343,6 +3351,14 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/base/notion-page-selector/page-selector/index.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"app/components/base/notion-page-selector/search-input/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
@@ -4818,6 +4834,16 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/datasets/documents/create-from-pipeline/data-source/online-documents/page-selector/index.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/datasets/documents/create-from-pipeline/data-source/online-documents/page-selector/item.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"app/components/datasets/documents/create-from-pipeline/data-source/online-documents/title.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
|
||||
@@ -71,7 +71,7 @@ export const OVERLAY_RESTRICTED_IMPORT_PATTERNS = [
|
||||
'**/base/dropdown',
|
||||
'**/base/dropdown/index',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/dropdown-menu instead. See issue #32767.',
|
||||
message: 'Deprecated: use @langgenius/dify-ui/dropdown-menu instead. See issue #32767.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
|
||||
@@ -135,9 +135,6 @@
|
||||
"dataSource.notion.pagesAuthorized": "Pages authorized",
|
||||
"dataSource.notion.remove": "Remove",
|
||||
"dataSource.notion.selector.addPages": "Add pages",
|
||||
"dataSource.notion.selector.configure": "Configure Notion",
|
||||
"dataSource.notion.selector.docs": "Notion docs",
|
||||
"dataSource.notion.selector.headerTitle": "Choose Notion pages",
|
||||
"dataSource.notion.selector.noSearchResult": "No search results",
|
||||
"dataSource.notion.selector.pageSelected": "Pages Selected",
|
||||
"dataSource.notion.selector.preview": "PREVIEW",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"analyze": "next experimental-analyze",
|
||||
"analyze-component": "node ./scripts/analyze-component.js",
|
||||
"build": "next build",
|
||||
"build:dify-ui": "pnpm --filter @langgenius/dify-ui build",
|
||||
"build:vinext": "vinext build",
|
||||
"dev": "next dev",
|
||||
"dev:inspect": "next dev --inspect",
|
||||
@@ -40,6 +41,16 @@
|
||||
"lint:quiet": "vp run lint --quiet",
|
||||
"lint:tss": "tsslint --project tsconfig.json",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"prebuild": "pnpm run build:dify-ui",
|
||||
"prebuild:vinext": "pnpm run build:dify-ui",
|
||||
"predev": "pnpm run build:dify-ui",
|
||||
"predev:vinext": "pnpm run build:dify-ui",
|
||||
"prestorybook": "pnpm run build:dify-ui",
|
||||
"prestorybook:build": "pnpm run build:dify-ui",
|
||||
"pretest": "pnpm run build:dify-ui",
|
||||
"pretest:watch": "pnpm run build:dify-ui",
|
||||
"pretype-check": "pnpm run build:dify-ui",
|
||||
"pretype-check:tsgo": "pnpm run build:dify-ui",
|
||||
"refactor-component": "node ./scripts/refactor-component.js",
|
||||
"start": "node ./scripts/copy-and-start.mjs",
|
||||
"start:vinext": "vinext start",
|
||||
@@ -61,6 +72,7 @@
|
||||
"@formatjs/intl-localematcher": "catalog:",
|
||||
"@headlessui/react": "catalog:",
|
||||
"@heroicons/react": "catalog:",
|
||||
"@langgenius/dify-ui": "workspace:*",
|
||||
"@lexical/code": "catalog:",
|
||||
"@lexical/link": "catalog:",
|
||||
"@lexical/list": "catalog:",
|
||||
@@ -81,7 +93,6 @@
|
||||
"@tailwindcss/typography": "catalog:",
|
||||
"@tanstack/react-form": "catalog:",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-virtual": "catalog:",
|
||||
"abcjs": "catalog:",
|
||||
"ahooks": "catalog:",
|
||||
"class-variance-authority": "catalog:",
|
||||
@@ -101,7 +112,6 @@
|
||||
"es-toolkit": "catalog:",
|
||||
"fast-deep-equal": "catalog:",
|
||||
"foxact": "catalog:",
|
||||
"hast-util-to-jsx-runtime": "catalog:",
|
||||
"html-entities": "catalog:",
|
||||
"html-to-image": "catalog:",
|
||||
"i18next": "catalog:",
|
||||
@@ -136,13 +146,14 @@
|
||||
"react-papaparse": "catalog:",
|
||||
"react-pdf-highlighter": "catalog:",
|
||||
"react-sortablejs": "catalog:",
|
||||
"react-syntax-highlighter": "catalog:",
|
||||
"react-textarea-autosize": "catalog:",
|
||||
"react-window": "catalog:",
|
||||
"reactflow": "catalog:",
|
||||
"remark-breaks": "catalog:",
|
||||
"remark-directive": "catalog:",
|
||||
"scheduler": "catalog:",
|
||||
"sharp": "catalog:",
|
||||
"shiki": "catalog:",
|
||||
"sortablejs": "catalog:",
|
||||
"std-semver": "catalog:",
|
||||
"streamdown": "catalog:",
|
||||
@@ -197,6 +208,8 @@
|
||||
"@types/qs": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/react-syntax-highlighter": "catalog:",
|
||||
"@types/react-window": "catalog:",
|
||||
"@types/sortablejs": "catalog:",
|
||||
"@typescript-eslint/parser": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
import commonConfig from './tailwind-common-config'
|
||||
import difyUiTailwindPreset from '@langgenius/dify-ui/tailwind-preset'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
@@ -10,7 +10,7 @@ const config: Config = {
|
||||
'./node_modules/@streamdown/math/dist/*.js',
|
||||
'!./**/*.{spec,test}.{js,ts,jsx,tsx}',
|
||||
],
|
||||
...commonConfig,
|
||||
...difyUiTailwindPreset,
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
Reference in New Issue
Block a user